From c61365a9c99fcddd8624512da6bb80175ecc6293 Mon Sep 17 00:00:00 2001 From: derailed Date: Tue, 18 Jun 2019 17:54:44 -0600 Subject: [PATCH] misc bug fixes --- README.md | 1 + go.mod | 3 - go.sum | 13 - internal/config/style.go | 29 +- internal/resource/base.go | 19 +- internal/views/alias.go | 22 +- internal/views/app.go | 7 +- internal/views/app_test.go | 24 +- internal/views/benchmark.go | 12 +- internal/views/command.go | 18 +- internal/views/command_test.go | 19 ++ internal/views/context.go | 4 +- internal/views/context_test.go | 33 +++ internal/views/dp.go | 4 +- internal/views/dp_test.go | 16 ++ internal/views/ds_test.go | 16 ++ internal/views/registrar.go | 483 +++++++++++++++++---------------- internal/views/table.go | 37 +-- internal/views/table_test.go | 103 +++++++ 19 files changed, 533 insertions(+), 330 deletions(-) create mode 100644 internal/views/command_test.go create mode 100644 internal/views/context_test.go create mode 100644 internal/views/dp_test.go create mode 100644 internal/views/ds_test.go diff --git a/README.md b/README.md index 969fe707..8a1de3f6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ for changes and offers subsequent commands to interact with observed Kubernetes --- [![Go Report Card](https://goreportcard.com/badge/github.com/derailed/k9s?)](https://goreportcard.com/report/github.com/derailed/k9s) +[![codebeat badge](https://codebeat.co/badges/89e5a80e-dfe8-4426-acf6-6be781e0a12e)](https://codebeat.co/projects/github-com-derailed-k9s-master) [![Build Status](https://travis-ci.com/derailed/k9s.svg?branch=master)](https://travis-ci.com/derailed/k9s) [![release](https://img.shields.io/github/release-pre/derailed/k9s.svg)](https://github.com/derailed/k9s/releases) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE) diff --git a/go.mod b/go.mod index 3da6caf6..e3468936 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/evanphx/json-patch v4.1.0+incompatible // indirect github.com/fatih/camelcase v1.0.0 // indirect - github.com/fatih/color v1.7.0 // indirect github.com/fsnotify/fsnotify v1.4.7 github.com/gdamore/tcell v1.1.1 github.com/gogo/protobuf v1.2.1 // indirect @@ -34,7 +33,6 @@ require ( github.com/imdario/mergo v0.3.7 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/json-iterator/go v1.1.6 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-runewidth v0.0.4 github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect @@ -45,7 +43,6 @@ require ( github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.3.0 - github.com/wercker/stern v0.0.0-20181017112310-807830e57719 github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 // indirect golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect diff --git a/go.sum b/go.sum index fd1ed656..9310417b 100644 --- a/go.sum +++ b/go.sum @@ -33,10 +33,6 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/derailed/tview v0.1.8 h1:HjkCCTzgZWkkyUtJMGxhRJtvIvcRLuPCb9Uh11KRoC4= -github.com/derailed/tview v0.1.8/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0= -github.com/derailed/tview v0.1.9 h1:CYyGBvhJ4VenoRlUE1NDstyv4kayjQVnidSDAwuemdk= -github.com/derailed/tview v0.1.9/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0= github.com/derailed/tview v0.1.10 h1:QWjK82ccTl3C7Tfyfmv765eRqEt/T3aXp40464cfnlw= github.com/derailed/tview v0.1.10/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -55,8 +51,6 @@ github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7Vpz github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= @@ -129,10 +123,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY= github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -193,8 +183,6 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/wercker/stern v0.0.0-20181017112310-807830e57719 h1:x9/CGytbUciiGoBHn04xsJJR/Lsg4b3qQlKJSZY5Gzw= -github.com/wercker/stern v0.0.0-20181017112310-807830e57719/go.mod h1:+72MfLYlS87s4tqq+eVDANQ9GdILz0lkpAFlX/1+WWY= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= @@ -243,7 +231,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190426135247-a129542de9ae h1:mQLHiymj/JXKnnjc62tb7nD5pZLs940/sXJu+Xp3DBA= golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/config/style.go b/internal/config/style.go index d1090e89..52bb7976 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -22,19 +22,18 @@ type ( // Style tracks K9s styles. Style struct { - FgColor string `yaml:"fgColor"` - BgColor string `yaml:"bgColor"` - LogoColor string `yaml:"logoColor"` - - Info *Info `yaml:"info"` - Border *Border `yaml:"border"` - Menu *Menu `yaml:"menu"` - Crumb *Crumb `yaml:"crumb"` - Table *Table `yaml:"table"` - Status *Status `yaml:"status"` - Title *Title `yaml:"title"` - Yaml *Yaml `yaml:"yaml"` - Log *Log `yaml:"logs"` + FgColor string `yaml:"fgColor"` + BgColor string `yaml:"bgColor"` + LogoColor string `yaml:"logoColor"` + Title *Title `yaml:"title"` + Border *Border `yaml:"border"` + Info *Info `yaml:"info"` + Menu *Menu `yaml:"menu"` + Crumb *Crumb `yaml:"crumb"` + Table *Table `yaml:"table"` + Status *Status `yaml:"status"` + Yaml *Yaml `yaml:"yaml"` + Log *Log `yaml:"logs"` } // Status tracks resource status styles. @@ -117,13 +116,13 @@ func newStyle() *Style { FgColor: "cadetblue", BgColor: "black", LogoColor: "orange", - Info: newInfo(), Border: newBorder(), + Title: newTitle(), + Info: newInfo(), Menu: newMenu(), Crumb: newCrumb(), Table: newTable(), Status: newStatus(), - Title: newTitle(), Yaml: newYaml(), Log: newLog(), } diff --git a/internal/resource/base.go b/internal/resource/base.go index 977af6d9..d9e1d0e3 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -3,12 +3,14 @@ package resource import ( "bytes" "context" + "fmt" "path" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions/printers" @@ -127,14 +129,23 @@ func (b *Base) List(ns string) (Columnars, error) { // Describe a given resource. func (b *Base) Describe(kind, pa string) (string, error) { - ns, n := namespaced(pa) - mapping, err := k8s.RestMapping.Find(kind) if err != nil { - log.Debug().Msgf("Unable to find mapper for %s %s", kind, pa) - return "", err + g, v, n := b.Resource.(*k8s.Resource).GetInfo() + mapper := k8s.RestMapper{b.Connection} + var e error + mapping, e = mapper.ResourceFor(fmt.Sprintf("%s.%s.%s", n, v, g)) + if e != nil { + log.Debug().Err(err).Msgf("Unable to find mapper for %s %s", kind, pa) + return "", err + } } + return b.doDescribe(pa, mapping) +} + +func (b *Base) doDescribe(pa string, mapping *meta.RESTMapping) (string, error) { + ns, n := namespaced(pa) d, err := versioned.Describer(b.Connection.Config().Flags(), mapping) if err != nil { log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping) diff --git a/internal/views/alias.go b/internal/views/alias.go index 5f193e94..1d1d5b1e 100644 --- a/internal/views/alias.go +++ b/internal/views/alias.go @@ -39,32 +39,23 @@ func (v *aliasView) init(context.Context, string) { v.update(v.hydrate()) v.app.SetFocus(v) v.resetTitle() + v.app.setHints(v.hints()) + } func (v *aliasView) registerActions() { + delete(v.actions, KeyShiftA) v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true) v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false) v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) - v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortResourceCmd, true) - v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortGroupCmd, true) + v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortColCmd(1), true) + v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortColCmd(2), true) } func (v *aliasView) getTitle() string { return aliasTitle } -func (v *aliasView) sortResourceCmd(evt *tcell.EventKey) *tcell.EventKey { - v.sortCol.index, v.sortCol.asc = 1, true - v.refresh() - return nil -} - -func (v *aliasView) sortGroupCmd(evt *tcell.EventKey) *tcell.EventKey { - v.sortCol.index, v.sortCol.asc = 2, true - v.refresh() - return nil -} - func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.cmdBuff.empty() { v.cmdBuff.reset() @@ -115,7 +106,8 @@ func (v *aliasView) hints() hints { } func (v *aliasView) hydrate() resource.TableData { - cmds := helpCmds(v.app.conn()) + cmds := make(map[string]resCmd, 40) + aliasCmds(v.app.conn(), cmds) data := resource.TableData{ Header: resource.Row{"NAME", "RESOURCE", "APIGROUP"}, diff --git a/internal/views/app.go b/internal/views/app.go index e3d5816c..18c28e5f 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -99,8 +99,10 @@ func (a *appView) registerActions() { } func (a *appView) Init(version string, rate int) { - a.startInformer() - a.clusterInfo().init(version) + if a.conn() != nil { + a.startInformer() + a.clusterInfo().init(version) + } a.cmdBuff.addListener(a.cmd()) header := tview.NewFlex() @@ -333,6 +335,7 @@ func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey { func (a *appView) currentView() igniter { return a.content.GetPrimitive("main").(igniter) } + func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { if a.inCmdMode() { return evt diff --git a/internal/views/app_test.go b/internal/views/app_test.go index 62bf8019..04184f49 100644 --- a/internal/views/app_test.go +++ b/internal/views/app_test.go @@ -1,18 +1,16 @@ package views -// import ( -// "testing" +import ( + "testing" -// "github.com/derailed/k9s/internal/config" -// "github.com/stretchr/testify/assert" -// ) + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) -// func TestNewApp(t *testing.T) { -// mk := NewMockKubeSettings() -// cfg := config.NewConfig(mk) -// a := NewApp(cfg) -// a.Init("blee", 10) +func TestNewApp(t *testing.T) { + a := NewApp(config.NewConfig(ks{})) + a.Init("blee", 10) -// assert.Equal(t, 10, len(a.actions)) -// assert.Equal(t, false, a.hasSkins) -// } + assert.Equal(t, 10, len(a.actions)) + assert.Equal(t, false, a.hasSkins) +} diff --git a/internal/views/benchmark.go b/internal/views/benchmark.go index 04c3adfd..e0bd8d5a 100644 --- a/internal/views/benchmark.go +++ b/internal/views/benchmark.go @@ -23,13 +23,11 @@ const ( // K9sBenchDir directory to store K9s benchmark files. var K9sBenchDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-bench-%s", config.MustK9sUser())) -type ( - benchmark struct { - canceled bool - config config.BenchConfig - worker *requester.Work - } -) +type benchmark struct { + canceled bool + config config.BenchConfig + worker *requester.Work +} func newBenchmark(base string, cfg config.BenchConfig) (*benchmark, error) { b := benchmark{config: cfg} diff --git a/internal/views/command.go b/internal/views/command.go index 350e9296..9e3a4247 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -68,7 +68,9 @@ func (c *command) run(cmd string) bool { return true } default: - if res, ok := resourceViews(c.app.conn())[cmd]; ok { + cmds := make(map[string]resCmd, 30) + resourceViews(c.app.conn(), cmds) + if res, ok := cmds[cmd]; ok { var r resource.List if res.listFn != nil { r = res.listFn(c.app.conn(), resource.DefaultNamespace) @@ -91,20 +93,22 @@ func (c *command) run(cmd string) bool { } } - res, ok := allCRDs(c.app.conn())[cmd] + cmds := make(map[string]resCmd, 30) + allCRDs(c.app.conn(), cmds) + res, ok := cmds[cmd] if !ok { c.app.flash().warnf("Huh? `%s` command not found", cmd) return false } - name := res.Plural - if len(name) == 0 { - name = res.Singular + name := res.plural + if name == "" { + name = res.singular } v = newResourceView( - res.Kind, + res.title, c.app, - resource.NewCustomList(c.app.conn(), "", res.Group, res.Version, name), + resource.NewCustomList(c.app.conn(), "", res.api, res.version, name), ) v.setColorerFn(defaultColorer) c.exec(cmd, v) diff --git a/internal/views/command_test.go b/internal/views/command_test.go new file mode 100644 index 00000000..864951c9 --- /dev/null +++ b/internal/views/command_test.go @@ -0,0 +1,19 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestCommandPush(t *testing.T) { + c := newCommand(NewApp(config.NewConfig(ks{}))) + c.pushCmd("fred") + c.pushCmd("blee") + p, top := c.previousCmd() + + assert.Equal(t, "fred", p) + assert.True(t, top) + assert.True(t, c.lastCmd()) +} diff --git a/internal/views/context.go b/internal/views/context.go index 74031646..91b108c4 100644 --- a/internal/views/context.go +++ b/internal/views/context.go @@ -10,8 +10,8 @@ type contextView struct { *resourceView } -func newContextView(t string, app *appView, list resource.List) resourceViewer { - v := contextView{newResourceView(t, app, list).(*resourceView)} +func newContextView(title string, app *appView, list resource.List) resourceViewer { + v := contextView{newResourceView(title, app, list).(*resourceView)} v.extraActionsFn = v.extraActions v.enterFn = v.useCtx v.getTV().cleanseFn = v.cleanser diff --git a/internal/views/context_test.go b/internal/views/context_test.go new file mode 100644 index 00000000..5d2a8c34 --- /dev/null +++ b/internal/views/context_test.go @@ -0,0 +1,33 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/resource" + "github.com/stretchr/testify/assert" +) + +func TestContextView(t *testing.T) { + l := resource.NewContextList(nil, "fred") + v := newContextView("blee", NewApp(config.NewConfig(ks{})), l) + + assert.Equal(t, "blee", v.getTitle()) +} + +func TestCleaner(t *testing.T) { + uu := map[string]struct { + s, e string + }{ + "normal": {"fred", "fred"}, + "default": {"fred*", "fred"}, + "delta": {"fred(𝜟)", "fred"}, + } + + v := contextView{} + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, v.cleanser(u.s)) + }) + } +} diff --git a/internal/views/dp.go b/internal/views/dp.go index a99ebf98..4d5826b9 100644 --- a/internal/views/dp.go +++ b/internal/views/dp.go @@ -12,8 +12,8 @@ type deployView struct { *logResourceView } -func newDeployView(ns string, app *appView, list resource.List) resourceViewer { - v := deployView{newLogResourceView(ns, app, list)} +func newDeployView(title string, app *appView, list resource.List) resourceViewer { + v := deployView{newLogResourceView(title, app, list)} v.extraActionsFn = v.extraActions v.enterFn = v.showPods diff --git a/internal/views/dp_test.go b/internal/views/dp_test.go new file mode 100644 index 00000000..0dcdb94c --- /dev/null +++ b/internal/views/dp_test.go @@ -0,0 +1,16 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/resource" + "github.com/stretchr/testify/assert" +) + +func TestDeployView(t *testing.T) { + l := resource.NewDeploymentList(nil, "fred") + v := newDeployView("blee", NewApp(config.NewConfig(ks{})), l) + + assert.Equal(t, "blee", v.getTitle()) +} diff --git a/internal/views/ds_test.go b/internal/views/ds_test.go new file mode 100644 index 00000000..32665e52 --- /dev/null +++ b/internal/views/ds_test.go @@ -0,0 +1,16 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/resource" + "github.com/stretchr/testify/assert" +) + +func TestDaemonSetView(t *testing.T) { + l := resource.NewDaemonSetList(nil, "fred") + v := newDaemonSetView("blee", NewApp(config.NewConfig(ks{})), l) + + assert.Equal(t, "blee", v.getTitle()) +} diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 9584077d..ec6d03f1 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -18,6 +18,9 @@ type ( resCmd struct { title string api string + version string + plural string + singular string viewFn viewFn listFn listFn enterFn enterFn @@ -26,25 +29,14 @@ type ( } ) -func helpCmds(c k8s.Connection) map[string]resCmd { - cmdMap := resourceViews(c) - cmds := make(map[string]resCmd, len(cmdMap)) - for k, v := range cmdMap { - cmds[k] = v +func aliasCmds(c k8s.Connection, m map[string]resCmd) { + resourceViews(c, m) + if c != nil { + allCRDs(c, m) } - for k, v := range allCRDs(c) { - cmds[k] = resCmd{title: v.Kind, api: v.Group} - } - - return cmds } -func allCRDs(c k8s.Connection) map[string]k8s.APIGroup { - m := map[string]k8s.APIGroup{} - if c == nil { - return m - } - +func allCRDs(c k8s.Connection, m map[string]resCmd) { crds, _ := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces). Resource(). List(resource.AllNamespaces) @@ -58,24 +50,23 @@ func allCRDs(c k8s.Connection) map[string]k8s.APIGroup { Version: ff["version"].(string), } + res := resCmd{title: grp.Kind, api: grp.Group, version: grp.Version} if p, ok := ff["plural"].(string); ok { - grp.Plural = p - m[p] = grp + res.plural = p + m[p] = res } if s, ok := ff["singular"].(string); ok { - grp.Singular = s - m[s] = grp + res.singular = s + m[s] = res } if aa, ok := ff["aliases"].([]interface{}); ok { for _, a := range aa { - m[a.(string)] = grp + m[a.(string)] = res } } } - - return m } func showRBAC(app *appView, ns, resource, selection string) { @@ -110,220 +101,254 @@ func showSAPolicy(app *appView, _, _, selection string) { app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n)) } -func resourceViews(c k8s.Connection) map[string]resCmd { - cmds := map[string]resCmd{ - "cm": { - title: "ConfigMaps", - api: "", - viewFn: newResourceView, - listFn: resource.NewConfigMapList, - }, - "cr": { - title: "ClusterRoles", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewClusterRoleList, - enterFn: showRBAC, - }, - "crb": { - title: "ClusterRoleBindings", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewClusterRoleBindingList, - enterFn: showClusterRole, - }, - "crd": { - title: "CustomResourceDefinitions", - api: "apiextensions.k8s.io", - viewFn: newResourceView, - listFn: resource.NewCustomResourceDefinitionList, - }, - "cj": { - title: "CronJobs", - api: "batch", - viewFn: newCronJobView, - listFn: resource.NewCronJobList, - }, - "ctx": { - title: "Contexts", - api: "", - viewFn: newContextView, - listFn: resource.NewContextList, - colorerFn: ctxColorer, - }, - "ds": { - title: "DaemonSets", - api: "", - viewFn: newDaemonSetView, - listFn: resource.NewDaemonSetList, - colorerFn: dpColorer, - }, - "dp": { - title: "Deployments", - api: "apps", - viewFn: newDeployView, - listFn: resource.NewDeploymentList, - colorerFn: dpColorer, - }, - "ep": { - title: "EndPoints", - api: "", - viewFn: newResourceView, - listFn: resource.NewEndpointsList, - }, - "ev": { - title: "Events", - api: "", - viewFn: newResourceView, - listFn: resource.NewEventList, - colorerFn: evColorer, - }, - "ing": { - title: "Ingress", - api: "extensions", - viewFn: newResourceView, - listFn: resource.NewIngressList, - }, - "jo": { - title: "Jobs", - api: "batch", - viewFn: newJobView, - listFn: resource.NewJobList, - }, - "no": { - title: "Nodes", - api: "", - viewFn: newNodeView, - listFn: resource.NewNodeList, - colorerFn: nsColorer, - }, - "ns": { - title: "Namespaces", - api: "", - viewFn: newNamespaceView, - listFn: resource.NewNamespaceList, - colorerFn: nsColorer, - }, - "pdb": { - title: "PodDisruptionBudgets", - api: "v1.beta1", - viewFn: newResourceView, - listFn: resource.NewPDBList, - colorerFn: pdbColorer, - }, - "po": { - title: "Pods", - api: "", - viewFn: newPodView, - listFn: resource.NewPodList, - colorerFn: podColorer, - }, - "pv": { - title: "PersistentVolumes", - api: "", - viewFn: newResourceView, - listFn: resource.NewPersistentVolumeList, - colorerFn: pvColorer, - }, - "pvc": { - title: "PersistentVolumeClaims", - api: "", - viewFn: newResourceView, - listFn: resource.NewPersistentVolumeClaimList, - colorerFn: pvcColorer, - }, - "rb": { - title: "RoleBindings", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewRoleBindingList, - enterFn: showRole, - }, - "rc": { - title: "ReplicationControllers", - api: "", - viewFn: newResourceView, - listFn: resource.NewReplicationControllerList, - colorerFn: rsColorer, - }, - "ro": { - title: "Roles", - api: "rbac.authorization.k8s.io", - viewFn: newResourceView, - listFn: resource.NewRoleList, - enterFn: showRBAC, - }, - "rs": { - title: "ReplicaSets", - api: "apps", - viewFn: newReplicaSetView, - listFn: resource.NewReplicaSetList, - colorerFn: rsColorer, - }, - "sa": { - title: "ServiceAccounts", - api: "", - viewFn: newResourceView, - listFn: resource.NewServiceAccountList, - enterFn: showSAPolicy, - }, - "sec": { - title: "Secrets", - api: "", - viewFn: newSecretView, - listFn: resource.NewSecretList, - }, - "sts": { - title: "StatefulSets", - api: "apps", - viewFn: newStatefulSetView, - listFn: resource.NewStatefulSetList, - colorerFn: stsColorer, - }, - "svc": { - title: "Services", - api: "", - viewFn: newSvcView, - listFn: resource.NewServiceList, - }, - "usr": { - title: "Users", - api: "", - viewFn: newSubjectView, - }, - "grp": { - title: "Groups", - api: "", - viewFn: newSubjectView, - }, - "pf": { - title: "PortForward", - api: "", - viewFn: newForwardView, - }, - "be": { - title: "Benchmark", - api: "", - viewFn: newBenchView, - }, - "sd": { - title: "ScreenDumps", - api: "", - viewFn: newDumpView, - }, +func resourceViews(c k8s.Connection, m map[string]resCmd) { + coreRes(m) + rbacRes(m) + apiExtRes(m) + batchRes(m) + appsRes(m) + extRes(m) + v1beta1Res(m) + custRes(m) + + if c != nil { + hpaRes(c, m) + } +} + +func coreRes(m map[string]resCmd) { + m["cm"] = resCmd{ + title: "ConfigMaps", + api: "", + viewFn: newResourceView, + listFn: resource.NewConfigMapList, + } + m["ctx"] = resCmd{ + title: "Contexts", + api: "", + viewFn: newContextView, + listFn: resource.NewContextList, + colorerFn: ctxColorer, + } + m["ds"] = resCmd{ + title: "DaemonSets", + api: "", + viewFn: newDaemonSetView, + listFn: resource.NewDaemonSetList, + colorerFn: dpColorer, + } + m["ep"] = resCmd{ + title: "EndPoints", + api: "", + viewFn: newResourceView, + listFn: resource.NewEndpointsList, + } + m["ev"] = resCmd{ + title: "Events", + api: "", + viewFn: newResourceView, + listFn: resource.NewEventList, + colorerFn: evColorer, + } + m["no"] = resCmd{ + title: "Nodes", + api: "", + viewFn: newNodeView, + listFn: resource.NewNodeList, + colorerFn: nsColorer, + } + m["ns"] = resCmd{ + title: "Namespaces", + api: "", + viewFn: newNamespaceView, + listFn: resource.NewNamespaceList, + colorerFn: nsColorer, + } + m["po"] = resCmd{ + title: "Pods", + api: "", + viewFn: newPodView, + listFn: resource.NewPodList, + colorerFn: podColorer, + } + m["pv"] = resCmd{ + title: "PersistentVolumes", + api: "", + viewFn: newResourceView, + listFn: resource.NewPersistentVolumeList, + colorerFn: pvColorer, + } + m["pvc"] = resCmd{ + title: "PersistentVolumeClaims", + api: "", + viewFn: newResourceView, + listFn: resource.NewPersistentVolumeClaimList, + colorerFn: pvcColorer, + } + m["rc"] = resCmd{ + title: "ReplicationControllers", + api: "", + viewFn: newResourceView, + listFn: resource.NewReplicationControllerList, + colorerFn: rsColorer, + } + m["sa"] = resCmd{ + title: "ServiceAccounts", + api: "", + viewFn: newResourceView, + listFn: resource.NewServiceAccountList, + enterFn: showSAPolicy, + } + m["sec"] = resCmd{ + title: "Secrets", + api: "", + viewFn: newSecretView, + listFn: resource.NewSecretList, + } + m["svc"] = resCmd{ + title: "Services", + api: "", + viewFn: newSvcView, + listFn: resource.NewServiceList, + } +} + +func custRes(m map[string]resCmd) { + m["usr"] = resCmd{ + title: "Users", + api: "", + viewFn: newSubjectView, + } + m["grp"] = resCmd{ + title: "Groups", + api: "", + viewFn: newSubjectView, + } + m["pf"] = resCmd{ + title: "PortForward", + api: "", + viewFn: newForwardView, + } + m["be"] = resCmd{ + title: "Benchmark", + api: "", + viewFn: newBenchView, + } + m["sd"] = resCmd{ + title: "ScreenDumps", + api: "", + viewFn: newDumpView, + } +} + +func rbacRes(m map[string]resCmd) { + m["cr"] = resCmd{ + title: "ClusterRoles", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewClusterRoleList, + enterFn: showRBAC, + } + m["crb"] = resCmd{ + title: "ClusterRoleBindings", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewClusterRoleBindingList, + enterFn: showClusterRole, } - if c == nil { - return cmds + m["rb"] = resCmd{ + title: "RoleBindings", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewRoleBindingList, + enterFn: showRole, } + m["ro"] = resCmd{ + title: "Roles", + api: "rbac.authorization.k8s.io", + viewFn: newResourceView, + listFn: resource.NewRoleList, + enterFn: showRBAC, + } +} + +func apiExtRes(m map[string]resCmd) { + m["crd"] = resCmd{ + title: "CustomResourceDefinitions", + api: "apiextensions.k8s.io", + viewFn: newResourceView, + listFn: resource.NewCustomResourceDefinitionList, + } +} + +func batchRes(m map[string]resCmd) { + m["cj"] = resCmd{ + title: "CronJobs", + api: "batch", + viewFn: newCronJobView, + listFn: resource.NewCronJobList, + } + m["jo"] = resCmd{ + title: "Jobs", + api: "batch", + viewFn: newJobView, + listFn: resource.NewJobList, + } +} + +func appsRes(m map[string]resCmd) { + m["dp"] = resCmd{ + title: "Deployments", + api: "apps", + viewFn: newDeployView, + listFn: resource.NewDeploymentList, + colorerFn: dpColorer, + } + m["rs"] = resCmd{ + title: "ReplicaSets", + api: "apps", + viewFn: newReplicaSetView, + listFn: resource.NewReplicaSetList, + colorerFn: rsColorer, + } + m["sts"] = resCmd{ + title: "StatefulSets", + api: "apps", + viewFn: newStatefulSetView, + listFn: resource.NewStatefulSetList, + colorerFn: stsColorer, + } +} + +func extRes(m map[string]resCmd) { + m["ing"] = resCmd{ + title: "Ingress", + api: "extensions", + viewFn: newResourceView, + listFn: resource.NewIngressList, + } +} + +func v1beta1Res(m map[string]resCmd) { + m["pdb"] = resCmd{ + title: "PodDisruptionBudgets", + api: "v1.beta1", + viewFn: newResourceView, + listFn: resource.NewPDBList, + colorerFn: pdbColorer, + } +} + +func hpaRes(c k8s.Connection, cmds map[string]resCmd) { rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"}) if err != nil { log.Error().Err(err).Msg("Checking HPA") - return cmds + return } if !ok { log.Error().Msg("HPA are not supported on this cluster") - return cmds + return } switch rev { @@ -351,6 +376,4 @@ func resourceViews(c k8s.Connection) map[string]resCmd { default: log.Panic().Msgf("K9s unsupported HPA version. Exiting!") } - - return cmds } diff --git a/internal/views/table.go b/internal/views/table.go index 3e7b4c66..04525504 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -49,14 +49,14 @@ type ( app *appView baseTitle string currentNS string + data resource.TableData actions keyActions + cmdBuff *cmdBuff colorerFn colorerFn sortFn sortFn cleanseFn cleanseFn - data resource.TableData - cmdBuff *cmdBuff - sortCol sortColumn filterFn func(string) + sortCol sortColumn } ) @@ -406,7 +406,15 @@ func (v *tableView) doUpdate(data resource.TableData) { fgColor = v.colorerFn(data.Namespace, data.Rows[sk]) } for col, field := range data.Rows[sk].Fields { - v.addBodyCell(data.NumCols, data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads) + header := data.Header[col] + field, align := v.formatCell(data.NumCols[header], header, field, pads[col]) + c := tview.NewTableCell(field + deltas(data.Rows[sk].Deltas[col], field)) + { + c.SetExpansion(1) + c.SetAlign(align) + c.SetTextColor(fgColor) + } + v.SetCell(row, col, c) } row++ } @@ -447,7 +455,7 @@ func (v *tableView) addHeaderCell(numCols map[string]bool, col int, name string, v.SetCell(0, col, c) } -func (v *tableView) addBodyCell(numCols map[string]bool, header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) { +func (v *tableView) formatCell(numerical bool, header, field string, padding int) (string, int) { if header == "AGE" { dur, err := time.ParseDuration(field) if err == nil { @@ -455,21 +463,16 @@ func (v *tableView) addBodyCell(numCols map[string]bool, header string, row, col } } - field += deltas(delta, field) - align := tview.AlignLeft - if numCols[header] || cpuRX.MatchString(header) || memRX.MatchString(header) { - align = tview.AlignRight - } else if isASCII(field) { - field = pad(field, pads[col]) + if numerical || cpuRX.MatchString(header) || memRX.MatchString(header) { + return field, tview.AlignRight } - c := tview.NewTableCell(field) - { - c.SetExpansion(1) - c.SetAlign(align) - c.SetTextColor(color) + align := tview.AlignLeft + if isASCII(field) { + return pad(field, padding), align } - v.SetCell(row, col, c) + + return field, align } func (v *tableView) defaultSort(rows resource.Rows, sortCol sortColumn) { diff --git a/internal/views/table_test.go b/internal/views/table_test.go index 2ddb482a..75630a6f 100644 --- a/internal/views/table_test.go +++ b/internal/views/table_test.go @@ -2,13 +2,116 @@ package views import ( "fmt" + "io/ioutil" + "path/filepath" "strings" "testing" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/watch" ) +func TestTableViewSave(t *testing.T) { + v := newTableView(NewApp(config.NewConfig(ks{})), "test") + v.baseTitle = "k9s-test" + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + c1, _ := ioutil.ReadDir(dir) + v.saveCmd(nil) + c2, _ := ioutil.ReadDir(dir) + assert.Equal(t, len(c2), len(c1)+1) +} + +func TestTableViewNew(t *testing.T) { + v := newTableView(NewApp(config.NewConfig(ks{})), "test") + + data := resource.TableData{ + Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"}, + Rows: resource.RowEvents{ + "ns1/a": &resource.RowEvent{ + Action: watch.Added, + Fields: resource.Row{"ns1", "a", "10", "3m"}, + Deltas: resource.Row{"", "", "", ""}, + }, + "ns1/b": &resource.RowEvent{ + Action: watch.Added, + Fields: resource.Row{"ns1", "b", "15", "1m"}, + Deltas: resource.Row{"", "", "20", ""}, + }, + }, + NumCols: map[string]bool{ + "FRED": true, + }, + Namespace: "", + } + v.update(data) + assert.Equal(t, 3, v.GetRowCount()) +} + +func TestTableViewFilter(t *testing.T) { + v := newTableView(NewApp(config.NewConfig(ks{})), "test") + + data := resource.TableData{ + Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"}, + Rows: resource.RowEvents{ + "ns1/blee": &resource.RowEvent{ + Action: watch.Added, + Fields: resource.Row{"ns1", "blee", "10", "3m"}, + Deltas: resource.Row{"", "", "", ""}, + }, + "ns1/fred": &resource.RowEvent{ + Action: watch.Added, + Fields: resource.Row{"ns1", "fred", "15", "1m"}, + Deltas: resource.Row{"", "", "20", ""}, + }, + }, + NumCols: map[string]bool{ + "FRED": true, + }, + Namespace: "", + } + v.update(data) + v.cmdBuff.setActive(true) + v.cmdBuff.buff = []rune("blee") + v.filterCmd(nil) + assert.Equal(t, 2, v.GetRowCount()) + v.resetCmd(nil) + assert.Equal(t, 3, v.GetRowCount()) +} + +func TestTableViewSort(t *testing.T) { + v := newTableView(NewApp(config.NewConfig(ks{})), "test") + + data := resource.TableData{ + Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"}, + Rows: resource.RowEvents{ + "ns1/blee": &resource.RowEvent{ + Action: watch.Added, + Fields: resource.Row{"ns1", "blee", "10", "3m"}, + Deltas: resource.Row{"", "", "", ""}, + }, + "ns1/fred": &resource.RowEvent{ + Action: watch.Added, + Fields: resource.Row{"ns1", "fred", "15", "1m"}, + Deltas: resource.Row{"", "", "20", ""}, + }, + }, + NumCols: map[string]bool{ + "FRED": true, + }, + Namespace: "", + } + v.update(data) + v.sortColCmd(1)(nil) + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, "blee ", v.GetCell(1, 1).Text) + + v.sortInvertCmd(nil) + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, "fred ", v.GetCell(1, 1).Text) +} + func TestIsSelector(t *testing.T) { uu := map[string]struct { sel string