diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..f9690f57 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,301 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # default concurrency is a available CPU number + concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 1m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: true + + # list of build tags, all linters use it. Default is empty list. + # build-tags: + # - mytag + + # which dirs to skip: issues from them won't be reported; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but default dirs are skipped independently + # from this option's value (see skip-dirs-use-default). + # skip-dirs: + # - src/external_libs + # - autogenerated_by_my_lib + + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + # skip-files: + # - ".*\\.my\\.go$" + # - lib/bad.go + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + # modules-download-mode: readonly|release|vendor + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + +# all available settings of specific linters +linters-settings: + errcheck: + # report about not checking of errors in type assetions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + + # [deprecated] comma-separated list of pairs of the form pkg:regex + # the regex is used to ignore names within pkg. (default "fmt:.*"). + # see https://github.com/kisielk/errcheck#the-deprecated-method for details + # ignore: fmt:.*,io/ioutil:^Read.* + # path to a file containing a list of functions to exclude from checking + # see https://github.com/kisielk/errcheck#excluding-functions for details + # exclude: /path/to/file.txt + + funlen: + lines: 60 + statements: 40 + + govet: + # report about shadowed variables + check-shadowing: true + + # settings per analyzer + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + + # enable or disable analyzers by name + enable: + - atomicalign + enable-all: false + disable: + - shadow + disable-all: false + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.8 + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/org/project + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + depguard: + list-type: blacklist + include-go-root: false + packages: + - github.com/sirupsen/logrus + packages-with-error-messages: + # specify an error message to output when a blacklisted package is used + github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - someword + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 1 + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + gocritic: + # Which checks should be enabled; can't be combined with 'disabled-checks'; + # See https://go-critic.github.io/overview#checks-overview + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` + # By default list of stable checks is used. + enabled-checks: + - rangeValCopy + + # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty + disabled-checks: + - regexpMust + + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - performance + + settings: # settings passed to gocritic + captLocal: # must be valid enabled check name + paramsOnly: true + rangeValCopy: + sizeThreshold: 32 + godox: + # report any comments starting with keywords, this is useful for TODO or FIXME comments that + # might be left in the code accidentally and should be resolved before merging + keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting + - NOTE + - OPTIMIZE # marks code that should be optimized before merging + - HACK # marks hack-arounds that should be removed before merging + dogsled: + # checks assignments with too many blank identifiers; default is 2 + max-blank-identifiers: 2 + + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + wsl: + # If true append is only allowed to be cuddled if appending value is + # matching variables, fields or types on line above. Default is true. + strict-append: true + # Allow calls and assignments to be cuddled as long as the lines have any + # matching variables, fields or types. Default is true. + allow-assign-and-call: true + # Allow multiline assignments to be cuddled. Default is true. + allow-multiline-assign: true + # Allow declarations (var) to be cuddled. + allow-cuddle-declarations: false + # Allow trailing comments in ending of blocks + allow-trailing-comment: false + # Force newlines in end of case at this limit (0 = never). + force-case-trailing-whitespace: 0 + +linters: + enable: + - megacheck + - govet + disable: + - maligned + - prealloc + disable-all: false + presets: + - bugs + - unused + fast: false + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently from this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - abcdef + + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + + # Exclude known linters from partially hard-vendored code, + # which is impossible to exclude via "nolint" comments. + - path: internal/hmac/ + text: "weak cryptographic primitive" + linters: + - gosec + + # Exclude some staticcheck messages + - linters: + - staticcheck + text: "SA9003:" + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + + # Independently from option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + # Show only new issues: if there are unstaged changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing + # large codebase. It's not practical to fix all existing issues at the moment + # of integration: much better don't allow issues in new code. + # Default is false. + new: false + + # Show only new issues created after git revision `REV` + new-from-rev: REV + + # Show only new issues created in git patch with set file path. + new-from-patch: path/to/patch/file diff --git a/change_logs/release_0.6.7.md b/change_logs/release_0.6.7.md index 33c482c9..20e53064 100644 --- a/change_logs/release_0.6.7.md +++ b/change_logs/release_0.6.7.md @@ -22,7 +22,6 @@ This is a maintenance release to mainly resolve outstanding issues and bugs. Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node view. - --- ## Resolved Bugs diff --git a/internal/model/menu_hint.go b/internal/model/menu_hint.go index c2341aae..47b076cf 100644 --- a/internal/model/menu_hint.go +++ b/internal/model/menu_hint.go @@ -12,6 +12,11 @@ type MenuHint struct { Visible bool } +// IsBlank checks if menu hint is a place holder. +func (m MenuHint) IsBlank() bool { + return m.Mnemonic == "" && m.Description == "" && m.Visible == false +} + // MenuHints represents a collection of hints. type MenuHints []MenuHint diff --git a/internal/model/stack.go b/internal/model/stack.go index ae36a97c..fc925135 100644 --- a/internal/model/stack.go +++ b/internal/model/stack.go @@ -74,14 +74,9 @@ func (s *Stack) RemoveListener(l StackListener) { // AddListener registers a stack listener. func (s *Stack) AddListener(l StackListener) { s.listeners = append(s.listeners, l) - log.Debug().Msgf("Stack Add listener %#v", s.components) - s.DumpStack() - if s.Empty() { - log.Debug().Msgf("Stack is empty!") - } else { - log.Debug().Msgf("TOP is %s", s.Top().Name()) + if !s.Empty() { + l.StackTop(s.Top()) } - l.StackTop(s.Top()) } // Dump prints out the stack. @@ -114,7 +109,7 @@ func (s *Stack) Pop() (Component, bool) { c.Stop() if top := s.Top(); top != nil { - log.Debug().Msgf("Calling Restart on %s", top.Name()) + log.Debug().Msgf("Calling Start on %s", top.Name()) top.Start() } @@ -131,6 +126,15 @@ func (s *Stack) IsLast() bool { return len(s.components) == 1 } +// Previous returns the previous component if any. +func (s *Stack) Previous() Component { + if s.IsLast() { + return s.Top() + } + + return s.components[len(s.components)-2] +} + // Top returns the top most item or nil if the stack is empty. func (s *Stack) Top() Component { if s.Empty() { diff --git a/internal/resource/container.go b/internal/resource/container.go index bd5ceec3..fbd0b850 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -64,11 +64,6 @@ func (r *Container) Marshal(path string) (string, error) { return "", nil } -// // PodLogs tail logs for all containers in a running Pod. -// func (r *Container) PodLogs(ctx context.Context, c chan<- string, ns, n string, lines int64, prev bool) error { -// return nil -// } - // Logs tails a given container logs func (r *Container) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { res, ok := r.Resource.(k8s.Loggable) diff --git a/internal/resource/pod.go b/internal/resource/pod.go index b03c0c15..dd568e74 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -55,7 +55,7 @@ type ( func NewPodList(c Connection, ns string) List { return NewList( ns, - "po", + "pods", NewPod(c), AllVerbsAccess|DescribeAccess, ) diff --git a/internal/ui/app.go b/internal/ui/app.go index c2cfc771..258b48d0 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -2,7 +2,6 @@ package ui import ( "github.com/derailed/k9s/internal/k8s" - "github.com/derailed/k9s/internal/model" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -13,7 +12,6 @@ type App struct { Configurator Main *Pages - Hint *model.Hint actions KeyActions @@ -28,7 +26,6 @@ func NewApp() *App { actions: make(KeyActions), Main: NewPages(), cmdBuff: NewCmdBuff(':', CommandBuff), - Hint: model.NewHint(), } a.RefreshStyles() @@ -50,8 +47,6 @@ func (a *App) Init() { a.SetInputCapture(a.keyboard) a.cmdBuff.AddListener(a.Cmd()) a.SetRoot(a.Main, true) - - a.Hint.AddListener(a.Menu()) } // Conn returns an api server connection. diff --git a/internal/ui/crumbs.go b/internal/ui/crumbs.go index 648c2b08..eea1d7a8 100644 --- a/internal/ui/crumbs.go +++ b/internal/ui/crumbs.go @@ -6,7 +6,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/tview" - "github.com/rs/zerolog/log" ) // Crumbs represents user breadcrumbs. @@ -35,14 +34,12 @@ func NewCrumbs(styles *config.Styles) *Crumbs { // StackPushed indicates a new item was added. func (v *Crumbs) StackPushed(c model.Component) { v.stack.Push(c) - log.Debug().Msgf(">>> PUSH %v", v.stack.Flatten()) v.refresh(v.stack.Flatten()) } // StackPopped indicates an item was deleted func (v *Crumbs) StackPopped(_, _ model.Component) { v.stack.Pop() - log.Debug().Msgf("<<< POP %v", v.stack.Flatten()) v.refresh(v.stack.Flatten()) } diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 11a0f47c..632066f0 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -17,6 +17,7 @@ import ( const ( menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s " maxRows = 7 + chopWidth = 20 ) var menuRX = regexp.MustCompile(`\d`) @@ -36,9 +37,16 @@ func NewMenu(styles *config.Styles) *Menu { return &v } -// HintsChanged updates the menu based on hints changing. -func (v *Menu) HintsChanged(hh model.MenuHints) { - v.HydrateMenu(hh) +func (v *Menu) StackPushed(c model.Component) { + v.HydrateMenu(c.Hints()) +} + +func (v *Menu) StackPopped(o, n model.Component) { + v.HydrateMenu(n.Hints()) +} + +func (v *Menu) StackTop(t model.Component) { + v.HydrateMenu(t.Hints()) } // HydrateMenu populate menu ui from hints. @@ -58,17 +66,34 @@ func (v *Menu) HydrateMenu(hh model.MenuHints) { } } +func (v *Menu) hasDigits(hh model.MenuHints) bool { + for _, h := range hh { + if !h.Visible { + continue + } + if menuRX.MatchString(h.Mnemonic) { + return true + } + } + return false +} + func (v *Menu) buildMenuTable(hh model.MenuHints) [][]string { table := make([]model.MenuHints, maxRows+1) colCount := (len(hh) / maxRows) + 1 + + if v.hasDigits(hh) { + colCount++ + } + for row := 0; row < maxRows; row++ { - table[row] = make(model.MenuHints, colCount+1) + table[row] = make(model.MenuHints, colCount) } var row, col int firstCmd := true - maxKeys := make([]int, colCount+1) + maxKeys := make([]int, colCount) for _, h := range hh { if !h.Visible { continue @@ -76,6 +101,9 @@ func (v *Menu) buildMenuTable(hh model.MenuHints) [][]string { isDigit := menuRX.MatchString(h.Mnemonic) if !isDigit && firstCmd { row, col, firstCmd = 0, col+1, false + if table[0][0].IsBlank() { + col = 0 + } } if maxKeys[col] < len(h.Mnemonic) { maxKeys[col] = len(h.Mnemonic) @@ -142,7 +170,7 @@ func formatNSMenu(i int, name string, styles config.Frame) string { fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1) fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1) fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1) - return fmt.Sprintf(fmat, i, Truncate(name, 14)) + return fmt.Sprintf(fmat, i, Truncate(name, chopWidth)) } func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string { diff --git a/internal/ui/table.go b/internal/ui/table.go index ace8337e..f71f8a15 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -60,6 +60,7 @@ func NewTable(title string) *Table { } func (t *Table) Init(ctx context.Context) { + log.Debug().Msgf("UI Table INIT %q", t.baseTitle) t.styles = ctx.Value(KeyStyles).(*config.Styles) t.SetFixed(1, 0) @@ -78,7 +79,6 @@ func (t *Table) Init(ctx context.Context) { t.SetSelectionChangedFunc(t.selChanged) t.SetInputCapture(t.keyboard) - } // SendKey sends an keyboard event (testing only!). diff --git a/internal/view/alias.go b/internal/view/alias.go index a0a24b39..18af43c6 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -55,7 +55,7 @@ func (a *Alias) registerActions() { a.RmAction(tcell.KeyCtrlS) a.AddActions(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true), + tcell.KeyEnter: ui.NewKeyAction("Goto Resource", a.gotoCmd, true), tcell.KeyEscape: ui.NewKeyAction("Reset", a.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", a.activateCmd, false), ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.SortColCmd(0), false), @@ -82,6 +82,7 @@ func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if r != 0 { s := ui.TrimCell(a.Table.Table, r, 1) tokens := strings.Split(s, ",") + a.app.Content.Pop() a.app.gotoResource(tokens[0], true) return nil } diff --git a/internal/view/app.go b/internal/view/app.go index eda7275c..28a55679 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -19,7 +19,6 @@ import ( const ( splashTime = 1 - devMode = "dev" clusterRefresh = time.Duration(5 * time.Second) indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" ) @@ -27,8 +26,6 @@ const ( // ActionsFunc augments Keybindinga. type ActionsFunc func(ui.KeyActions) -type focusHandler func(tview.Primitive) - type forwarder interface { Start(path, co string, ports []string) (*portforward.PortForwarder, error) Stop() @@ -86,7 +83,9 @@ func (a *App) ActiveView() model.Component { } func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey { - a.Content.Pop() + if !a.Content.IsLast() { + a.Content.Pop() + } return nil } @@ -95,6 +94,7 @@ func (a *App) Init(version string, rate int) { ctx := context.WithValue(context.Background(), ui.KeyApp, a) a.Content.Init(ctx) a.Content.Stack.AddListener(a.Crumbs()) + a.Content.Stack.AddListener(a.Menu()) a.version = version a.CmdBuff().AddListener(a) @@ -122,15 +122,6 @@ func (a *App) Init(version string, rate int) { a.Main.AddPage("main", main, true, false) a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) - // ctx := context.WithValue(context.Background(), ui.KeyApp, a) - // a.Content.Init(ctx) - // d := NewDetails(a, nil) - // d.SetText("Fuck!!") - // a.Content.Push(d) - // d = NewDetails(a, nil) - // d.SetText("Shit!!") - // a.Content.Push(d) - main.AddItem(a.indicator(), 1, 1, false) main.AddItem(a.Content, 0, 10, true) main.AddItem(a.Crumbs(), 2, 1, false) @@ -138,31 +129,12 @@ func (a *App) Init(version string, rate int) { a.toggleHeader(!a.Config.K9s.GetHeadless()) } -// func (a *App) StackPushed(c model.Component) { -// ctx := context.WithValue(context.Background(), ui.KeyApp, a) -// ctx, a.cancelFn = context.WithCancel(context.Background()) -// c.Init(ctx) - -// a.Frame().AddPage(c.Name(), c, true, true) -// a.SetFocus(c) -// a.setHints(c.Hints()) -// } - -// func (a *App) StackPopped(o, c model.Component) { -// a.Frame().RemovePage(o.Name()) -// if c != nil { -// a.StackPushed(c) -// } -// } - -// func (a *App) StackTop(model.Component) { -// } - // Changed indicates the buffer was changed. func (a *App) BufferChanged(s string) {} // Active indicates the buff activity changed. func (a *App) BufferActive(state bool, _ ui.BufferKind) { + log.Debug().Msgf("App Buffer Activated!") flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) if !ok { return @@ -258,7 +230,9 @@ func (a *App) switchNS(ns string) bool { log.Debug().Msgf("Namespace did not change %s", ns) return true } - a.Config.SetActiveNamespace(ns) + if err := a.Config.SetActiveNamespace(ns); err != nil { + log.Error().Err(err).Msg("Config Set NS failed!") + } return a.startInformer(ns) } @@ -276,7 +250,9 @@ func (a *App) switchCtx(ctx string, load bool) error { } a.startInformer(ns) a.Config.Reset() - a.Config.Save() + if err := a.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } a.Flash().Infof("Switching context to %s", ctx) if load { a.gotoResource("po", true) diff --git a/internal/view/bench.go b/internal/view/bench.go index a339471c..224956d9 100644 --- a/internal/view/bench.go +++ b/internal/view/bench.go @@ -12,7 +12,6 @@ import ( "time" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/perf" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" @@ -22,8 +21,7 @@ import ( ) const ( - benchTitle = "Benchmarks" - benchTitleFmt = " [seagreen::b]%s([fuchsia::b]%d[fuchsia::-])[seagreen::-] " + benchTitle = "Benchmarks" ) var ( @@ -42,13 +40,14 @@ type Bench struct { cancelFn context.CancelFunc } -func NewBench(title, gvr string, _ resource.List) ResourceViewer { +// NewBench returns a new viewer. +func NewBench(_, _ string, _ resource.List) ResourceViewer { return &Bench{ - MasterDetail: NewMasterDetail(title), + MasterDetail: NewMasterDetail(benchTitle, ""), } } -// Init the view. +// Init initializes the viewer. func (b *Bench) Init(ctx context.Context) { b.MasterDetail.Init(ctx) b.keyBindings() @@ -101,7 +100,7 @@ func (b *Bench) refresh() { func (b *Bench) keyBindings() { aa := ui.KeyActions{ - ui.KeyP: ui.NewKeyAction("Previous", b.app.PrevCmd, false), + tcell.KeyEsc: ui.NewKeyAction("Back", b.app.PrevCmd, false), tcell.KeyEnter: ui.NewKeyAction("Enter", b.enterCmd, false), tcell.KeyCtrlD: ui.NewKeyAction("Delete", b.deleteCmd, false), } @@ -112,16 +111,6 @@ func (b *Bench) getTitle() string { return benchTitle } -func (b *Bench) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - tv := b.masterPage() - tv.SetSortCol(tv.NameColIndex()+col, 0, asc) - tv.Refresh() - - return nil - } -} - func (b *Bench) enterCmd(evt *tcell.EventKey) *tcell.EventKey { if b.masterPage().SearchBuff().IsActive() { return b.masterPage().filterCmd(evt) @@ -172,14 +161,6 @@ func (b *Bench) benchFile() string { return ui.TrimCell(b.masterPage().Table, r, 7) } -func (b *Bench) Hints() model.MenuHints { - if h, ok := b.CurrentPage().Item.(model.Hinter); ok { - return h.Hints() - } - - return nil -} - func (b *Bench) hydrate() resource.TableData { ff, err := loadBenchDir(b.app.Config) if err != nil { diff --git a/internal/view/bench_test.go b/internal/view/bench_int_test.go similarity index 100% rename from internal/view/bench_test.go rename to internal/view/bench_int_test.go diff --git a/internal/view/colorer.go b/internal/view/colorer.go index 68e548f5..ba59393b 100644 --- a/internal/view/colorer.go +++ b/internal/view/colorer.go @@ -60,6 +60,8 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color { case "Completed": return ui.CompletedColor case "Running": + case "Terminating": + return ui.KillColor default: c = ui.ErrColor } diff --git a/internal/view/command.go b/internal/view/command.go index d509f987..001b5544 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -100,7 +100,10 @@ func (c *command) run(cmd string) bool { switch cmds[0] { case "ctx", "context", "contexts": if len(cmds) == 2 { - c.app.switchCtx(cmds[1], true) + if err := c.app.switchCtx(cmds[1], true); err != nil { + log.Error().Err(err).Msg("Context switch failed!") + return false + } return true } view := c.componentFor(gvr, v) diff --git a/internal/view/container.go b/internal/view/container.go index 550fe281..e2344e2f 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -1,6 +1,7 @@ package view import ( + "context" "errors" "fmt" "strings" @@ -25,13 +26,19 @@ func NewContainer(title string, list resource.List, path string) ResourceViewer LogResource: NewLogResource(title, "", list), } c.path = &path + + return &c +} + +// Init initializes the viewer. +func (c *Container) Init(ctx context.Context) { c.envFn = c.k9sEnv c.containerFn = c.selectedContainer c.extraActionsFn = c.extraActions c.enterFn = c.viewLogs c.colorerFn = containerColorer - return &c + c.LogResource.Init(ctx) } // Start starts the component. @@ -49,8 +56,6 @@ func (c *Container) extraActions(aa ui.KeyActions) { aa[ui.KeyShiftF] = ui.NewKeyAction("PortForward", c.portFwdCmd, true) aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", c.prevLogsCmd, true) aa[ui.KeyS] = ui.NewKeyAction("Shell", c.shellCmd, true) - aa[tcell.KeyEscape] = ui.NewKeyAction("Back", c.backCmd, false) - aa[ui.KeyP] = ui.NewKeyAction("Previous", c.backCmd, false) aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", c.sortColCmd(6, false), false) aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", c.sortColCmd(7, false), false) aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", c.sortColCmd(8, false), false) @@ -169,7 +174,3 @@ func (c *Container) runForward(pf *k8s.PortForward, f *portforward.PortForwarder pf.SetActive(false) }) } - -func (c *Container) backCmd(evt *tcell.EventKey) *tcell.EventKey { - return c.app.PrevCmd(evt) -} diff --git a/internal/view/container_test.go b/internal/view/container_test.go new file mode 100644 index 00000000..5cf98c0d --- /dev/null +++ b/internal/view/container_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestContainerNew(t *testing.T) { + po := view.NewContainer("Container", resource.NewContainerList(nil, nil), "fred/blee") + po.Init(makeCtx()) + + assert.Equal(t, "containers", po.Name()) + assert.Equal(t, 22, len(po.Hints())) +} diff --git a/internal/view/context.go b/internal/view/context.go index 6da72e8e..8a9e6013 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -53,7 +53,9 @@ func (c *Context) useContext(name string) error { return err } - c.app.switchCtx(name, false) + if err := c.app.switchCtx(name, false); err != nil { + return err + } c.refresh() if tv, ok := c.GetPrimitive("ctx").(*Table); ok { tv.Select(1, 0) diff --git a/internal/view/details.go b/internal/view/details.go index ab5f12d8..b367dcb3 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -18,7 +18,7 @@ import ( const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " -// Details presents a generic text viewer. +// Details represents a generic text viewer. type Details struct { *tview.TextView @@ -40,6 +40,7 @@ func NewDetails(app *App, backFn ui.ActionHandler) *Details { } } +// Init initializes the viewer. func (d *Details) Init(ctx context.Context) { d.app = ctx.Value(ui.KeyApp).(*App) @@ -63,9 +64,14 @@ func (d *Details) Init(ctx context.Context) { }) } +// Name returns the component name. func (d *Details) Name() string { return "details" } -func (d *Details) Start() {} -func (d *Details) Stop() {} + +// Start starts the view updater. +func (d *Details) Start() {} + +// Stop terminates the updater. +func (d *Details) Stop() {} func (d *Details) bindKeys() { d.actions = ui.KeyActions{ @@ -219,10 +225,7 @@ func (d *Details) setActions(aa ui.KeyActions) { // Hints fetch mmemonic and hints func (d *Details) Hints() model.MenuHints { - if d.actions != nil { - return d.actions.Hints() - } - return nil + return d.actions.Hints() } func (d *Details) refreshTitle() { diff --git a/internal/view/help.go b/internal/view/help.go index 361749a1..2673f1e2 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -20,10 +20,6 @@ const ( helpTitleFmt = " [aqua::b]%s " ) -type helpItem struct { - key, description string -} - // Help presents a help viewer. type Help struct { *ui.Table @@ -49,13 +45,16 @@ func (v *Help) Init(ctx context.Context) { v.SetBorderPadding(0, 0, 1, 1) v.SetInputCapture(v.keyboard) v.bindKeys() - v.build(v.app.Hint.Peek()) + v.build(v.app.Content.Previous().Hints()) } -func (v *Help) Name() string { return helpTitle } -func (v *Help) Start() {} -func (v *Help) Stop() {} -func (v *Help) Hints() model.MenuHints { return v.actions.Hints() } +func (v *Help) Name() string { return helpTitle } +func (v *Help) Start() {} +func (v *Help) Stop() {} +func (v *Help) Hints() model.MenuHints { + log.Debug().Msgf("Help Hints %#v", v.actions.Hints()) + return v.actions.Hints() +} func (v *Help) bindKeys() { v.actions = ui.KeyActions{ @@ -165,10 +164,6 @@ func (v *Help) showGeneral() model.MenuHints { Mnemonic: "Shift-i", Description: "Invert Sort", }, - { - Mnemonic: "p", - Description: "Previous View", - }, { Mnemonic: ":q", Description: "Quit", diff --git a/internal/view/help_test.go b/internal/view/help_test.go index ce5e0317..1cdb3159 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -1,29 +1,35 @@ package view_test -// import ( -// "testing" +import ( + "testing" -// "github.com/derailed/k9s/internal/config" -// "github.com/derailed/k9s/internal/model" -// "github.com/stretchr/testify/assert" -// v1 "k8s.io/api/core/v1" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// ) + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) -// func newNS(n string) v1.Namespace { -// return v1.Namespace{ObjectMeta: metav1.ObjectMeta{ -// Name: n, -// }} -// } +func newNS(n string) v1.Namespace { + return v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: n, + }} +} -// func TestHelpNew(t *testing.T) { -// a := view.NewApp(config.NewConfig(ks{})) -// v := view.NewHelp() -// ctx := context.WithValue(ui.KeyApp, app) -// v.Init(ctx) +func TestHelpNew(t *testing.T) { + ctx := makeCtx() -// app.SetHints(model.MenuHints{{Mnemonic: "blee", Description: "duh"}}) + app := ctx.Value(ui.KeyApp).(*view.App) + po := view.NewPod("Pod", "blee", resource.NewPodList(nil, "")) + po.Init(ctx) + app.Content.Push(po) -// assert.Equal(t, "", v.GetCell(1, 0).Text) -// assert.Equal(t, "duh", v.GetCell(1, 1).Text) -// } + v := view.NewHelp() + v.Init(ctx) + + assert.Equal(t, 32, v.GetRowCount()) + assert.Equal(t, 10, v.GetColumnCount()) + assert.Equal(t, "", v.GetCell(1, 0).Text) + assert.Equal(t, "Back", v.GetCell(1, 1).Text) +} diff --git a/internal/view/job.go b/internal/view/job.go index e5f9d21a..2d80f07e 100644 --- a/internal/view/job.go +++ b/internal/view/job.go @@ -24,7 +24,7 @@ func (j *Job) extraActions(aa ui.KeyActions) { j.LogResource.extraActions(aa) } -func (j *Job) showPods(app *App, ns, res, sel string) { +func (j *Job) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) job, err := k8s.NewJob(app.Conn()).Get(ns, n) if err != nil { diff --git a/internal/view/log.go b/internal/view/log.go index fa717aed..13a0606a 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "strings" - "sync/atomic" "time" "github.com/derailed/k9s/internal/config" @@ -17,44 +16,34 @@ import ( "github.com/rs/zerolog/log" ) -type logFrame struct { +// Log represents a generic log viewer. +type Log struct { *tview.Flex - app *App - actions ui.KeyActions - backFn ui.ActionHandler + app *App + actions ui.KeyActions + backFn ui.ActionHandler + logs *Details + scrollIndicator *AutoScrollIndicator + ansiWriter io.Writer + path string } -func newLogFrame(app *App, backFn ui.ActionHandler) *logFrame { - f := logFrame{ +// NewLog returns a new viewer. +func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log { + l := Log{ Flex: tview.NewFlex(), app: app, backFn: backFn, actions: make(ui.KeyActions), } - f.SetBorder(true) - f.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor)) - f.SetBorderPadding(0, 0, 1, 1) - f.SetDirection(tview.FlexRow) + l.SetBorder(true) + l.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor)) + l.SetBorderPadding(0, 0, 1, 1) + l.SetDirection(tview.FlexRow) - return &f -} - -type Log struct { - *logFrame - - logs *Details - status *statusView - ansiWriter io.Writer - autoScroll int32 - path string -} - -func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log { - l := Log{ - logFrame: newLogFrame(app, backFn), - autoScroll: 1, - } + l.scrollIndicator = NewAutoScrollIndicator(app.Styles) + l.AddItem(l.scrollIndicator, 1, 1, false) l.logs = NewDetails(app, backFn) { @@ -67,8 +56,6 @@ func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log { l.logs.SetMaxBuffer(app.Config.K9s.LogBufferSize) } l.ansiWriter = tview.ANSIWriter(l.logs, app.Styles.Views().Log.FgColor, app.Styles.Views().Log.BgColor) - l.status = newStatusView(app.Styles) - l.AddItem(l.status, 1, 1, false) l.AddItem(l.logs, 0, 1, true) l.bindKeys() @@ -77,16 +64,26 @@ func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log { return &l } +// Logs return the viewer logs. +func (l *Log) Logs() *Details { + return l.logs +} + +// ScrollIndicator returns the scroll mode viewer. +func (l *Log) ScrollIndicator() *AutoScrollIndicator { + return l.scrollIndicator +} + func (l *Log) bindKeys() { l.actions = ui.KeyActions{ tcell.KeyEscape: ui.NewKeyAction("Back", l.backCmd, true), ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), - ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleScrollCmd, true), + ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false), ui.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false), ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false), ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, true), + tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), } } @@ -124,32 +121,24 @@ func (l *Log) log(lines string) { log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount()) } -func (l *Log) flush(index int, buff []string) { - if index == 0 { +// Flush write logs to viewer. +func (l *Log) Flush(index int, buff []string) { + if index == 0 || !l.scrollIndicator.AutoScroll() { return } - if atomic.LoadInt32(&l.autoScroll) == 1 { - l.log(strings.Join(buff[:index], "\n")) - l.app.QueueUpdateDraw(func() { - l.updateIndicator() - l.logs.ScrollToEnd() - }) - } -} - -func (l *Log) updateIndicator() { - status := "Off" - if l.autoScroll == 1 { - status = "On" - } - l.status.update([]string{fmt.Sprintf("Autoscroll: %s", status)}) + l.log(strings.Join(buff[:index], "\n")) + l.app.QueueUpdateDraw(func() { + l.scrollIndicator.Refresh() + l.logs.ScrollToEnd() + }) } // ---------------------------------------------------------------------------- // Actions... -func (l *Log) saveCmd(evt *tcell.EventKey) *tcell.EventKey { +// SaveCmd dumps the logs to file. +func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.path, l.logs.GetText(true)); err != nil { l.app.Flash().Err(err) } else { @@ -183,28 +172,18 @@ func saveData(cluster, name, data string) (string, error) { log.Error().Err(err).Msgf("LogFile create %s", path) return "", nil } - if _, err := fmt.Fprintf(file, data); err != nil { + + if _, err := file.Write([]byte(data)); err != nil { return "", err } return path, nil } -func (l *Log) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey { - if atomic.LoadInt32(&l.autoScroll) == 0 { - atomic.StoreInt32(&l.autoScroll, 1) - } else { - atomic.StoreInt32(&l.autoScroll, 0) - } - - if atomic.LoadInt32(&l.autoScroll) == 1 { - l.app.Flash().Info("Autoscroll is on.") - l.logs.ScrollToEnd() - } else { - l.logs.LineUp() - l.app.Flash().Info("Autoscroll is off.") - } - l.updateIndicator() +// ToggleAutoScrollCmd toggles auto scrolling of logs. +func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { + l.scrollIndicator.ToggleAutoScroll() + l.scrollIndicator.Refresh() return nil } diff --git a/internal/view/log_resource.go b/internal/view/log_resource.go index 8b764e6c..43c5fb4b 100644 --- a/internal/view/log_resource.go +++ b/internal/view/log_resource.go @@ -6,6 +6,7 @@ import ( "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) // ContainerFn returns the active container name. @@ -85,8 +86,9 @@ func (l *LogResource) showLogs(prev bool) { } func (l *LogResource) backCmd(evt *tcell.EventKey) *tcell.EventKey { - // Reset namespace to what it was - l.app.Config.SetActiveNamespace(l.list.GetNamespace()) + if err := l.app.Config.SetActiveNamespace(l.list.GetNamespace()); err != nil { + log.Error().Err(err).Msg("Config NS set failed!") + } l.app.inject(l) return nil diff --git a/internal/view/log_test.go b/internal/view/log_test.go index 22c57a07..d8719630 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -1,81 +1,81 @@ package view_test -// import ( -// "bytes" -// "fmt" -// "testing" +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" + "testing" -// "github.com/derailed/k9s/internal/config" -// "github.com/derailed/k9s/internal/view" -// "github.com/derailed/tview" -// "github.com/stretchr/testify/assert" -// ) + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/derailed/tview" + "github.com/stretchr/testify/assert" +) -// func TestAnsi(t *testing.T) { -// buff := bytes.NewBufferString("") -// w := tview.ANSIWriter(buff, "white", "black") -// fmt.Fprintf(w, "[YELLOW] ok") -// assert.Equal(t, "[YELLOW] ok", buff.String()) +func TestAnsi(t *testing.T) { + buff := bytes.NewBufferString("") + w := tview.ANSIWriter(buff, "white", "black") + fmt.Fprintf(w, "[YELLOW] ok") + assert.Equal(t, "[YELLOW] ok", buff.String()) -// v := tview.NewTextView() -// v.SetDynamicColors(true) -// aw := tview.ANSIWriter(v, "white", "black") -// s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]" -// fmt.Fprintf(aw, s) -// assert.Equal(t, s+"\n", v.GetText(false)) -// } + v := tview.NewTextView() + v.SetDynamicColors(true) + aw := tview.ANSIWriter(v, "white", "black") + s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]" + fmt.Fprintf(aw, s) + assert.Equal(t, s+"\n", v.GetText(false)) +} -// func TestLogFlush(t *testing.T) { -// v := view.NewLog("Logs", NewApp(config.NewConfig(ks{})), nil) -// v.flush(2, []string{"blee", "bozo"}) +func TestLogFlush(t *testing.T) { + v := view.NewLog("Logs", makeApp(), nil) + v.Flush(2, []string{"blee", "bozo"}) -// v.toggleScrollCmd(nil) -// assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true)) -// assert.Equal(t, " Autoscroll: Off ", v.status.GetText(true)) -// v.toggleScrollCmd(nil) -// assert.Equal(t, " Autoscroll: On ", v.status.GetText(true)) -// } + v.ToggleAutoScrollCmd(nil) + assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true)) + assert.Equal(t, " Autoscroll: Off ", v.ScrollIndicator().GetText(true)) + v.ToggleAutoScrollCmd(nil) + assert.Equal(t, " Autoscroll: On ", v.ScrollIndicator().GetText(true)) + assert.Equal(t, 8, len(v.Hints())) +} -// func TestLogViewSave(t *testing.T) { -// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) -// v.flush(2, []string{"blee", "bozo"}) -// v.path = "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 TestLogViewSave(t *testing.T) { + app := makeApp() + v := view.NewLog("Logs", app, nil) + v.Flush(2, []string{"blee", "bozo"}) + config.K9sDumpDir = "/tmp" + dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) + c1, _ := ioutil.ReadDir(dir) + v.SaveCmd(nil) + c2, _ := ioutil.ReadDir(dir) + assert.Equal(t, len(c2), len(c1)+1) +} -// func TestLogViewNav(t *testing.T) { -// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) -// var buff []string -// v.autoScroll = 1 -// for i := 0; i < 100; i++ { -// buff = append(buff, fmt.Sprintf("line-%d\n", i)) -// } -// v.flush(100, buff) +func TestLogViewNav(t *testing.T) { + v := view.NewLog("Logs", makeApp(), nil) + var buff []string + for i := 0; i < 100; i++ { + buff = append(buff, fmt.Sprintf("line-%d\n", i)) + } + v.Flush(100, buff) + v.ToggleAutoScrollCmd(nil) -// v.topCmd(nil) -// r, _ := v.logs.GetScrollOffset() -// assert.Equal(t, 0, r) -// v.pageDownCmd(nil) -// r, _ = v.logs.GetScrollOffset() -// assert.Equal(t, 0, r) -// v.pageUpCmd(nil) -// r, _ = v.logs.GetScrollOffset() -// assert.Equal(t, 0, r) -// v.bottomCmd(nil) -// r, _ = v.logs.GetScrollOffset() -// assert.Equal(t, 0, r) -// } + r, _ := v.Logs().GetScrollOffset() + assert.Equal(t, -1, r) +} -// func TestLogViewClear(t *testing.T) { -// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) -// v.flush(2, []string{"blee", "bozo"}) +func TestLogViewClear(t *testing.T) { + v := view.NewLog("Logs", makeApp(), nil) + v.Flush(2, []string{"blee", "bozo"}) -// v.toggleScrollCmd(nil) -// assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true)) -// v.clearCmd(nil) -// assert.Equal(t, "", v.logs.GetText(true)) -// } + v.ToggleAutoScrollCmd(nil) + assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true)) + v.Logs().Clear() + assert.Equal(t, "", v.Logs().GetText(true)) +} + +// Helpers... + +func makeApp() *view.App { + return view.NewApp(config.NewConfig(ks{})) +} diff --git a/internal/view/logs.go b/internal/view/logs.go index f041cc14..5b4ecc66 100644 --- a/internal/view/logs.go +++ b/internal/view/logs.go @@ -14,31 +14,24 @@ import ( const ( logBuffSize = 100 - flushTimeout = 200 * time.Millisecond + FlushTimeout = 200 * time.Millisecond logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " logFmt = " Logs([fg:bg:]%s) " ) -type ( - masterView interface { - backFn() ui.ActionHandler - App() *App - } +// Logs presents a collection of logs. +type Logs struct { + *ui.Pages - // Logs presents a collection of logs. - Logs struct { - *ui.Pages - - app *App - parent loggable - actions ui.KeyActions - cancelFunc context.CancelFunc - } -) + app *App + parent Loggable + actions ui.KeyActions + cancelFunc context.CancelFunc +} // NewLogs returns a new logs viewer. -func NewLogs(title string, parent loggable) *Logs { +func NewLogs(title string, parent Loggable) *Logs { return &Logs{ Pages: ui.NewPages(), parent: parent, @@ -55,7 +48,7 @@ func (l *Logs) Name() string { return "logs" } // Protocol... -func (l *Logs) reload(co string, parent loggable, prevLogs bool) { +func (l *Logs) reload(co string, parent Loggable, prevLogs bool) { l.parent = parent l.deletePage() l.AddPage("logs", NewLog(co, l.app, l.backCmd), true, true) @@ -152,7 +145,7 @@ func updateLogs(ctx context.Context, c <-chan string, l *Log, buffSize int) { case line, ok := <-c: if !ok { log.Debug().Msgf("Closed channel detected. Bailing out...") - l.flush(index, buff) + l.Flush(index, buff) return } if index < buffSize { @@ -160,12 +153,12 @@ func updateLogs(ctx context.Context, c <-chan string, l *Log, buffSize int) { index++ continue } - l.flush(index, buff) + l.Flush(index, buff) index = 0 buff[index] = line index++ - case <-time.After(flushTimeout): - l.flush(index, buff) + case <-time.After(FlushTimeout): + l.Flush(index, buff) index = 0 case <-ctx.Done(): return diff --git a/internal/view/master_detail.go b/internal/view/master_detail.go index 5db9ff24..88fd89a5 100644 --- a/internal/view/master_detail.go +++ b/internal/view/master_detail.go @@ -3,7 +3,11 @@ package view import ( "context" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) // MasterDetail presents a master-detail viewer. @@ -18,23 +22,42 @@ type MasterDetail struct { } // NewMasterDetail returns a new master-detail viewer. -func NewMasterDetail(title string) *MasterDetail { +func NewMasterDetail(title, ns string) *MasterDetail { return &MasterDetail{ PageStack: NewPageStack(), title: title, + currentNS: ns, } } // Init initializes the viewer. func (m *MasterDetail) Init(ctx context.Context) { + log.Debug().Msgf("\t>>>MasterDetail init %q", m.title) + app := ctx.Value(ui.KeyApp).(*App) + if m.currentNS != resource.NotNamespaced { + m.currentNS = app.Config.ActiveNamespace() + } m.PageStack.Init(ctx) + m.AddListener(app.Menu()) t := NewTable(m.title) - t.Init(ctx) m.Push(t) - m.details = NewDetails(m.app, nil) + m.details = NewDetails(m.app, func(evt *tcell.EventKey) *tcell.EventKey { + m.Pop() + return nil + }) m.details.Init(ctx) + log.Debug().Msgf("\t<<<>>>>PS CHNGED<<<<<") - p.DumpStack() - active := p.CurrentPage() - if active == nil { - return - } - c := active.Item.(model.Component) - log.Debug().Msgf("-------Page activated %#v", active) - p.app.Hint.SetHints(c.Hints()) - }) - - p.Pages.SetTitle("Fuck!") p.Stack.AddListener(p) } func (p *PageStack) StackPushed(c model.Component) { ctx := context.WithValue(context.Background(), ui.KeyApp, p.app) c.Init(ctx) + c.Start() p.app.SetFocus(c) - p.app.Hint.SetHints(c.Hints()) } func (p *PageStack) StackPopped(o, top model.Component) { @@ -57,5 +42,4 @@ func (p *PageStack) StackTop(top model.Component) { } top.Start() p.app.SetFocus(top) - p.app.Hint.SetHints(top.Hints()) } diff --git a/internal/view/pod.go b/internal/view/pod.go index 77e2420b..0cf9141d 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -1,6 +1,7 @@ package view import ( + "context" "fmt" "github.com/derailed/k9s/internal/model" @@ -18,12 +19,14 @@ const ( shellCheck = "command -v bash >/dev/null && exec bash || exec sh" ) -type loggable interface { +type Loggable interface { getSelection() string getList() resource.List Pop() (model.Component, bool) } +var _ Loggable = &Pod{} + // Pod represents a pod viewer. type Pod struct { *Resource @@ -34,20 +37,23 @@ type Pod struct { // NewPod returns a new viewer. func NewPod(title, gvr string, list resource.List) ResourceViewer { - p := Pod{ + return &Pod{ Resource: NewResource(title, gvr, list), } +} + +// Init initializes the viewer. +func (p *Pod) Init(ctx context.Context) { p.extraActionsFn = p.extraActions p.enterFn = p.listContainers + p.Resource.Init(ctx) - p.picker = newSelectList(&p) + p.picker = newSelectList(p) p.picker.setActions(ui.KeyActions{ tcell.KeyEscape: {Description: "Back", Action: p.backCmd, Visible: true}, }) - - p.logs = NewLogs(list.GetName(), &p) - - return &p + p.logs = NewLogs(p.list.GetName(), p) + p.logs.Init(ctx) } func (p *Pod) extraActions(aa ui.KeyActions) { @@ -143,9 +149,8 @@ func (p *Pod) viewLogs(prev bool) bool { return true } -func (p *Pod) showLogs(path, co string, parent loggable, prev bool) { - l := p.GetPrimitive("logs").(*Logs) - l.reload(co, parent, prev) +func (p *Pod) showLogs(path, co string, parent Loggable, prev bool) { + p.logs.reload(co, parent, prev) p.Push(p.logs) } @@ -180,16 +185,6 @@ func (p *Pod) shellIn(path, co string) { p.Start() } -func (p *Pod) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := p.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 1980877c..1e1607e3 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -1,15 +1,27 @@ package view_test import ( + "context" "testing" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" ) func TestPodNew(t *testing.T) { - po := view.NewPod("test", "blee", resource.NewPodList(nil, "")) + po := view.NewPod("Pod", "blee", resource.NewPodList(nil, "")) + po.Init(makeCtx()) - assert.Equal(t, "po", po.Name()) + assert.Equal(t, "pods", po.Name()) + assert.Equal(t, 31, len(po.Hints())) +} + +// Helpers... + +func makeCtx() context.Context { + cfg := config.NewConfig(ks{}) + return context.WithValue(context.Background(), ui.KeyApp, view.NewApp(cfg)) } diff --git a/internal/view/policy.go b/internal/view/policy.go index 0a356c23..427d5ac2 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" @@ -85,9 +84,8 @@ func (p *Policy) bindKeys() { p.RmAction(ui.KeyShiftA) p.AddActions(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Reset", p.resetCmd, false), + tcell.KeyEscape: ui.NewKeyAction("Back", p.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", p.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", p.app.PrevCmd, false), ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", p.SortColCmd(0), false), ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.SortColCmd(1), false), ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.SortColCmd(2), false), @@ -130,10 +128,6 @@ func (p *Policy) backCmd(evt *tcell.EventKey) *tcell.EventKey { return p.app.PrevCmd(evt) } -func (p *Policy) Hints() model.MenuHints { - return p.Hints() -} - func (p *Policy) reconcile() (resource.TableData, error) { var table resource.TableData diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index 6a62d0b1..6042f019 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -19,14 +19,14 @@ import ( ) const ( - forwardTitle = "Port Forwards" - forwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] " - promptPage = "prompt" + portForwardTitle = "PortForwards" + portForwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] " + promptPage = "prompt" ) // PortForward presents active portforward viewer. type PortForward struct { - *ui.Pages + *MasterDetail cancelFn context.CancelFunc bench *perf.Benchmark @@ -34,27 +34,26 @@ type PortForward struct { } // NewPortForward returns a new viewer. -func NewPortForward(title, _ string, list resource.List) ResourceViewer { +func NewPortForward(title, gvr string, list resource.List) ResourceViewer { return &PortForward{ - Pages: ui.NewPages(), + MasterDetail: NewMasterDetail(portForwardTitle, ""), } } // Init the view. func (p *PortForward) Init(ctx context.Context) { p.app = ctx.Value(ui.KeyApp).(*App) + p.MasterDetail.Init(ctx) + p.registerActions() - tv := NewTable(forwardTitle) - tv.Init(ctx) + tv := p.masterPage() tv.SetBorderFocusColor(tcell.ColorDodgerBlue) tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone) tv.SetColorerFn(forwardColorer) tv.SetActiveNS("") tv.SetSortCol(tv.NameColIndex()+6, 0, true) tv.Select(1, 0) - p.Push(tv) - p.registerActions() p.Start() p.refresh() } @@ -71,11 +70,7 @@ func (p *PortForward) Start() { func (p *PortForward) Stop() {} func (p *PortForward) Name() string { - return "portForwards" -} - -func (p *PortForward) masterPage() *Table { - return p.GetPrimitive("table").(*Table) + return portForwardTitle } func (p *PortForward) setEnterFn(enterFn) {} @@ -83,13 +78,6 @@ func (p *PortForward) setColorerFn(ui.ColorerFunc) {} func (p *PortForward) setDecorateFn(decorateFn) {} func (p *PortForward) setExtraActionsFn(ActionsFunc) {} -func (p *PortForward) getTV() *Table { - if vu, ok := p.GetPrimitive("table").(*Table); ok { - return vu - } - return nil -} - func (p *PortForward) reload() { path := ui.BenchConfig(p.app.Config.K9s.CurrentCluster) log.Debug().Msgf("Reloading Config %s", path) @@ -100,38 +88,28 @@ func (p *PortForward) reload() { } func (p *PortForward) refresh() { - tv := p.getTV() + tv := p.masterPage() tv.Update(p.hydrate()) p.app.SetFocus(tv) tv.UpdateTitle() } func (p *PortForward) registerActions() { - tv := p.getTV() + tv := p.masterPage() tv.AddActions(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoBenchCmd, true), + tcell.KeyEnter: ui.NewKeyAction("Benchmarks", p.gotoBenchCmd, true), tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true), tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true), tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true), ui.KeySlash: ui.NewKeyAction("Filter", tv.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", p.app.PrevCmd, false), + tcell.KeyEsc: ui.NewKeyAction("Back", p.app.PrevCmd, false), ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.sortColCmd(2, true), false), ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.sortColCmd(4, true), false), }) } func (p *PortForward) getTitle() string { - return forwardTitle -} - -func (p *PortForward) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - tv := p.getTV() - tv.SetSortCol(tv.NameColIndex()+col, 0, asc) - p.refresh() - - return nil - } + return portForwardTitle } func (p *PortForward) gotoBenchCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -162,7 +140,7 @@ func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - tv := p.getTV() + tv := p.masterPage() r, _ := tv.GetSelection() cfg, co := defaultConfig(), ui.TrimCell(tv.Table, r, 2) if b, ok := p.app.Bench.Benchmarks.Containers[containerID(sel, co)]; ok { @@ -205,7 +183,7 @@ func (p *PortForward) runBenchmark() { } func (p *PortForward) getSelectedItem() string { - tv := p.getTV() + tv := p.masterPage() r, _ := tv.GetSelection() if r == 0 { return "" @@ -217,7 +195,7 @@ func (p *PortForward) getSelectedItem() string { } func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - tv := p.getTV() + tv := p.masterPage() if !tv.SearchBuff().Empty() { tv.SearchBuff().Reset() return nil @@ -238,7 +216,7 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { delete(p.app.forwarders, sel) log.Debug().Msgf("PortForwards after delete: %#v", p.app.forwarders) - p.getTV().Update(p.hydrate()) + p.masterPage().Update(p.hydrate()) p.app.Flash().Infof("PortForward %s deleted!", sel) }) @@ -250,7 +228,7 @@ func (p *PortForward) backCmd(evt *tcell.EventKey) *tcell.EventKey { p.cancelFn() } - tv := p.getTV() + tv := p.masterPage() if tv.SearchBuff().IsActive() { tv.SearchBuff().Reset() } else { @@ -260,10 +238,6 @@ func (p *PortForward) backCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (p *PortForward) Hints() model.MenuHints { - return p.getTV().Hints() -} - func (p *PortForward) hydrate() resource.TableData { data := initHeader(len(p.app.forwarders)) dc, dn := p.app.Bench.Benchmarks.Defaults.C, p.app.Bench.Benchmarks.Defaults.N @@ -293,7 +267,7 @@ func (p *PortForward) hydrate() resource.TableData { } func (p *PortForward) resetTitle() { - p.SetTitle(fmt.Sprintf(forwardTitleFmt, forwardTitle, p.getTV().GetRowCount()-1)) + p.SetTitle(fmt.Sprintf(portForwardTitleFmt, portForwardTitle, p.masterPage().GetRowCount()-1)) } // ---------------------------------------------------------------------------- @@ -354,7 +328,6 @@ func showModal(p *ui.Pages, msg, back string, ok func()) { func dismissModal(p *ui.Pages, page string) { p.RemovePage(promptPage) - p.SwitchToPage(page) } func watchFS(ctx context.Context, app *App, dir, file string, cb func()) error { diff --git a/internal/view/port_forward_test.go b/internal/view/port_forward_test.go new file mode 100644 index 00000000..04d9dfed --- /dev/null +++ b/internal/view/port_forward_test.go @@ -0,0 +1,16 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestPortForwardNew(t *testing.T) { + po := view.NewPortForward("", "", nil) + po.Init(makeCtx()) + + assert.Equal(t, "PortForwards", po.Name()) + assert.Equal(t, 15, len(po.Hints())) +} diff --git a/internal/view/port_selector.go b/internal/view/port_selector.go index 225aa15a..05ad4186 100644 --- a/internal/view/port_selector.go +++ b/internal/view/port_selector.go @@ -10,15 +10,6 @@ type portSelector struct { ok, cancel func() } -func newSelector(title, port string, okFn, cancelFn func()) *portSelector { - return &portSelector{ - title: title, - port: port, - ok: okFn, - cancel: cancelFn, - } -} - func (p *portSelector) show(app *App) { f := tview.NewForm() f.SetItemPadding(0) diff --git a/internal/view/rbac.go b/internal/view/rbac.go index a25daa25..4d63c357 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -15,11 +15,11 @@ import ( ) const ( - clusterRole roleKind = iota - role + ClusterRole roleKind = iota + Role all = "*" - rbacTitle = "RBAC" + rbacTitle = "Rbac" rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])" ) @@ -49,15 +49,6 @@ var ( "delete", } - httpVerbs = []string{ - "get", - "post", - "put", - "patch", - "delete", - "options", - } - httpTok8sVerbs = map[string]string{ "post": "create", "put": "update", @@ -66,8 +57,8 @@ var ( type roleKind = int8 -// RBAC presents an RBAC policy viewer. -type RBAC struct { +// Rbac presents an RBAC policy viewer. +type Rbac struct { *Table app *App @@ -77,24 +68,24 @@ type RBAC struct { cache resource.RowEvents } -// NewRBAC returns a new viewer. -func NewRBAC(app *App, ns, name string, kind roleKind) *RBAC { - r := RBAC{ +// NewRbac returns a new viewer. +func NewRbac(app *App, ns, name string, kind roleKind) *Rbac { + r := Rbac{ app: app, roleName: name, roleType: kind, } r.Table = NewTable(r.getTitle()) - r.SetActiveNS(ns) - r.SetColorerFn(rbacColorer) - r.bindKeys() return &r } // Init initializes the view. -func (r *RBAC) Init(ctx context.Context) { +func (r *Rbac) Init(ctx context.Context) { + r.SetActiveNS(r.app.Config.ActiveNamespace()) + r.SetColorerFn(rbacColorer) r.Table.Init(ctx) + r.bindKeys() r.Start() r.SetSortCol(1, len(rbacHeader), true) @@ -102,7 +93,11 @@ func (r *RBAC) Init(ctx context.Context) { } // Start watches for viewer updates -func (r *RBAC) Start() { +func (r *Rbac) Start() { + if r.app.Conn() == nil { + return + } + r.Stop() var ctx context.Context @@ -123,33 +118,35 @@ func (r *RBAC) Start() { } // Stop terminates the viewer updater. -func (r *RBAC) Stop() { +func (r *Rbac) Stop() { if r.cancelFn != nil { r.cancelFn() } } // Name returns the component name. -func (r *RBAC) Name() string { +func (r *Rbac) Name() string { return rbacTitle } -func (r *RBAC) bindKeys() { +func (r *Rbac) bindKeys() { r.RmAction(ui.KeyShiftA) r.AddActions(ui.KeyActions{ tcell.KeyEscape: ui.NewKeyAction("Reset", r.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", r.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", r.app.PrevCmd, false), ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", r.SortColCmd(1), false), }) } -func (r *RBAC) getTitle() string { +func (r *Rbac) getTitle() string { return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, r.roleName), r.app.Styles.Frame()) } -func (r *RBAC) refresh() { +func (r *Rbac) refresh() { + if r.app.Conn() == nil { + return + } data, err := r.reconcile(r.ActiveNS(), r.roleName, r.roleType) if err != nil { log.Error().Err(err).Msgf("Refresh for %s:%d", r.roleName, r.roleType) @@ -158,7 +155,8 @@ func (r *RBAC) refresh() { r.Update(data) } -func (r *RBAC) resetCmd(evt *tcell.EventKey) *tcell.EventKey { +func (r *Rbac) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("!!!YO!!!!") if !r.SearchBuff().Empty() { r.SearchBuff().Reset() return nil @@ -167,7 +165,8 @@ func (r *RBAC) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return r.backCmd(evt) } -func (r *RBAC) backCmd(evt *tcell.EventKey) *tcell.EventKey { +func (r *Rbac) backCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("!!!!RBAC back!!!") if r.cancelFn != nil { r.cancelFn() } @@ -180,7 +179,7 @@ func (r *RBAC) backCmd(evt *tcell.EventKey) *tcell.EventKey { return r.app.PrevCmd(evt) } -func (r *RBAC) reconcile(ns, name string, kind roleKind) (resource.TableData, error) { +func (r *Rbac) reconcile(ns, name string, kind roleKind) (resource.TableData, error) { var table resource.TableData evts, err := r.rowEvents(ns, name, kind) @@ -191,28 +190,28 @@ func (r *RBAC) reconcile(ns, name string, kind roleKind) (resource.TableData, er return buildTable(r, evts), nil } -func (r *RBAC) header() resource.Row { +func (r *Rbac) header() resource.Row { return rbacHeader } -func (r *RBAC) getCache() resource.RowEvents { +func (r *Rbac) getCache() resource.RowEvents { return r.cache } -func (r *RBAC) setCache(evts resource.RowEvents) { +func (r *Rbac) setCache(evts resource.RowEvents) { r.cache = evts } -func (r *RBAC) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) { +func (r *Rbac) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) { var ( evts resource.RowEvents err error ) switch kind { - case clusterRole: + case ClusterRole: evts, err = r.clusterPolicies(name) - case role: + case Role: evts, err = r.namespacedPolicies(name) default: return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind) @@ -225,7 +224,7 @@ func (r *RBAC) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, er return evts, nil } -func (r *RBAC) clusterPolicies(name string) (resource.RowEvents, error) { +func (r *Rbac) clusterPolicies(name string) (resource.RowEvents, error) { cr, err := r.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{}) if err != nil { return nil, err @@ -234,7 +233,7 @@ func (r *RBAC) clusterPolicies(name string) (resource.RowEvents, error) { return r.parseRules(cr.Rules), nil } -func (r *RBAC) namespacedPolicies(path string) (resource.RowEvents, error) { +func (r *Rbac) namespacedPolicies(path string) (resource.RowEvents, error) { ns, na := namespaced(path) cr, err := r.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{}) if err != nil { @@ -244,7 +243,7 @@ func (r *RBAC) namespacedPolicies(path string) (resource.RowEvents, error) { return r.parseRules(cr.Rules), nil } -func (r *RBAC) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents { +func (r *Rbac) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents { m := make(resource.RowEvents, len(rules)) for _, r := range rules { for _, grp := range r.APIGroups { diff --git a/internal/view/rbac_int_test.go b/internal/view/rbac_int_test.go new file mode 100644 index 00000000..095313b4 --- /dev/null +++ b/internal/view/rbac_int_test.go @@ -0,0 +1,115 @@ +package view + +import ( + "testing" + + "github.com/derailed/k9s/internal/resource" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestHasVerb(t *testing.T) { + uu := []struct { + vv []string + v string + e bool + }{ + {[]string{"*"}, "get", true}, + {[]string{"get", "list", "watch"}, "watch", true}, + {[]string{"get", "dope", "list"}, "watch", false}, + {[]string{"get"}, "get", true}, + {[]string{"post"}, "create", true}, + {[]string{"put"}, "update", true}, + {[]string{"list", "deletecollection"}, "deletecollection", true}, + } + + for _, u := range uu { + assert.Equal(t, u.e, hasVerb(u.vv, u.v)) + } +} + +func TestAsVerbs(t *testing.T) { + ok, nok := toVerbIcon(true), toVerbIcon(false) + + uu := []struct { + vv []string + e resource.Row + }{ + {[]string{"*"}, resource.Row{ok, ok, ok, ok, ok, ok, ok, ok, ""}}, + {[]string{"get", "list", "patch"}, resource.Row{ok, ok, nok, nok, nok, ok, nok, nok, ""}}, + {[]string{"get", "list", "deletecollection", "post"}, resource.Row{ok, ok, ok, nok, ok, nok, nok, nok, ""}}, + {[]string{"get", "list", "blee"}, resource.Row{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}}, + } + + for _, u := range uu { + assert.Equal(t, u.e, asVerbs(u.vv...)) + } +} + +func TestParseRules(t *testing.T) { + ok, nok := toVerbIcon(true), toVerbIcon(false) + _ = nok + + uu := []struct { + pp []rbacv1.PolicyRule + e map[string]resource.Row + }{ + { + []rbacv1.PolicyRule{ + {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}, + }, + map[string]resource.Row{ + "*.*": {"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}}, + }, + map[string]resource.Row{ + "*.*": {"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}}, + }, + map[string]resource.Row{ + "*": {"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}}, + }, + map[string]resource.Row{ + "pods": {"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, + "pods/fred": {"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}}, + }, + map[string]resource.Row{ + "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}, + }, + }, + { + []rbacv1.PolicyRule{ + {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}}, + }, + map[string]resource.Row{ + "/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}, + }, + }, + } + + var v Rbac + for _, u := range uu { + evts := v.parseRules(u.pp) + for k, v := range u.e { + assert.Equal(t, v, evts[k].Fields) + } + } +} diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index 1ab0eccd..2cb9f546 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -1,12 +1,22 @@ -package view +package view_test -// import ( -// "testing" +import ( + "testing" -// "github.com/derailed/k9s/internal/resource" -// "github.com/stretchr/testify/assert" -// rbacv1 "k8s.io/api/rbac/v1" -// ) + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestRbacNew(t *testing.T) { + cfg := config.NewConfig(ks{}) + app := view.NewApp(cfg) + v := view.NewRbac(app, "", "fred", view.ClusterRole) + v.Init(makeCtx()) + + assert.Equal(t, "Rbac", v.Name()) + assert.Equal(t, 10, len(v.Hints())) +} // func TestHasVerb(t *testing.T) { // uu := []struct { @@ -105,7 +115,7 @@ package view // }, // } -// var v rbacView +// var v view.Rbac // for _, u := range uu { // evts := v.parseRules(u.pp) // for k, v := range u.e { diff --git a/internal/view/registrar.go b/internal/view/registrar.go index a1f28285..2f6b6087 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -78,11 +78,11 @@ func allCRDs(c k8s.Connection, vv viewers) { } func showRBAC(app *App, ns, resource, selection string) { - kind := clusterRole + kind := ClusterRole if resource == "role" { - kind = role + kind = Role } - app.inject(NewRBAC(app, ns, selection, kind)) + app.inject(NewRbac(app, ns, selection, kind)) } func showCRD(app *App, ns, resource, selection string) { @@ -98,7 +98,7 @@ func showClusterRole(app *App, ns, resource, selection string) { app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection) return } - app.inject(NewRBAC(app, ns, crb.RoleRef.Name, clusterRole)) + app.inject(NewRbac(app, ns, crb.RoleRef.Name, ClusterRole)) } func showRole(app *App, _, resource, selection string) { @@ -108,7 +108,7 @@ func showRole(app *App, _, resource, selection string) { app.Flash().Errf("Unable to retrieve rolebindings for %s", selection) return } - app.inject(NewRBAC(app, ns, fqn(ns, rb.RoleRef.Name), role)) + app.inject(NewRbac(app, ns, fqn(ns, rb.RoleRef.Name), Role)) } func showSAPolicy(app *App, _, _, selection string) { diff --git a/internal/view/resource.go b/internal/view/resource.go index c37c63bc..ebc78b5f 100644 --- a/internal/view/resource.go +++ b/internal/view/resource.go @@ -9,7 +9,6 @@ import ( "github.com/atotto/clipboard" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -37,7 +36,7 @@ type Resource struct { // NewResource returns a new viewer. func NewResource(title, gvr string, list resource.List) *Resource { return &Resource{ - MasterDetail: NewMasterDetail(title), + MasterDetail: NewMasterDetail(title, list.GetNamespace()), list: list, gvr: gvr, } @@ -49,19 +48,23 @@ func (r *Resource) Init(ctx context.Context) { r.envFn = r.defaultK9sEnv table := r.masterPage() - table.setFilterFn(r.filterResource) - colorer := ui.DefaultColorer - if r.colorerFn != nil { - colorer = r.colorerFn + { + table.setFilterFn(r.filterResource) + colorer := ui.DefaultColorer + if r.colorerFn != nil { + colorer = r.colorerFn + } + table.SetColorerFn(colorer) } - table.SetColorerFn(colorer) - row, _ := table.GetSelection() - if row == 0 && table.GetRowCount() > 0 { - table.Select(1, 0) - } - r.DumpPages() r.refresh() + { + row, _ := table.GetSelection() + if row == 0 && table.GetRowCount() > 0 { + table.Select(1, 0) + } + } + log.Debug().Msgf("<<<< RESOURCE INIT") } // Start initializes updates. @@ -84,18 +87,6 @@ func (r *Resource) Name() string { return r.list.GetName() } -// Hints returns the current viewer hints -func (r *Resource) Hints() model.MenuHints { - if r.CurrentPage() == nil { - return nil - } - if c, ok := r.CurrentPage().Item.(model.Hinter); ok { - return c.Hints() - } - - return nil -} - func (r *Resource) setColorerFn(f ui.ColorerFunc) { r.colorerFn = f } @@ -131,19 +122,6 @@ func (r *Resource) backCmd(*tcell.EventKey) *tcell.EventKey { return nil } -func (r *Resource) switchPage1(p string) { - log.Debug().Msgf("Switching page to %s", p) - if _, ok := r.CurrentPage().Item.(*Table); ok { - r.Stop() - } - - r.SwitchToPage(p) - - if _, ok := r.CurrentPage().Item.(*Table); ok { - r.Start() - } -} - // ---------------------------------------------------------------------------- // Actions... @@ -210,9 +188,7 @@ func (r *Resource) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { } } r.refresh() - }, func() { - r.Pop() - }) + }, func() {}) return nil } @@ -283,7 +259,7 @@ func (r *Resource) viewCmd(evt *tcell.EventKey) *tcell.EventKey { details.SetTextColor(r.app.Styles.FgColor()) details.SetText(colorizeYAML(r.app.Styles.Views().Yaml, raw)) details.ScrollToBeginning() - r.app.Content.Push(details) + r.showDetails() return nil } @@ -313,6 +289,7 @@ func (r *Resource) editCmd(evt *tcell.EventKey) *tcell.EventKey { func (r *Resource) setNamespace(ns string) { if r.list.Namespaced() { + r.currentNS = ns r.list.SetNamespace(ns) } } @@ -335,32 +312,34 @@ func (r *Resource) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { r.masterPage().SelectRow(1, true) r.app.CmdBuff().Reset() if err := r.app.Config.SetActiveNamespace(r.currentNS); err != nil { + log.Error().Err(err).Msg("Config save NS failed!") + } + if err := r.app.Config.Save(); err != nil { log.Error().Err(err).Msg("Config save failed!") } - r.app.Config.Save() return nil } func (r *Resource) refresh() { - if r.CurrentPage() == nil { - return - } - if _, ok := r.CurrentPage().Item.(*Table); !ok { + if _, ok := r.Top().(*Table); !ok { return } - r.refreshActions() if r.list.Namespaced() { r.list.SetNamespace(r.currentNS) } - if err := r.list.Reconcile(r.app.informer, r.path); err != nil { - r.app.Flash().Err(err) + + if r.app.Conn() != nil { + if err := r.list.Reconcile(r.app.informer, r.path); err != nil { + r.app.Flash().Err(err) + } } data := r.list.Data() if r.decorateFn != nil { data = r.decorateFn(data) } + r.refreshActions() r.masterPage().Update(data) } diff --git a/internal/view/restartable_resource.go b/internal/view/restartable_resource.go index 255b7b29..e7f42c37 100644 --- a/internal/view/restartable_resource.go +++ b/internal/view/restartable_resource.go @@ -40,9 +40,7 @@ func (r *RestartableResource) restartCmd(evt *tcell.EventKey) *tcell.EventKey { } else { r.app.Flash().Infof("Rollout restart in progress for `%s...", sel) } - }, func() { - r.showMaster() - }) + }, func() {}) return nil } diff --git a/internal/view/rs.go b/internal/view/rs.go index 8e816dfe..7b503732 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -41,17 +41,7 @@ func (r *ReplicaSet) extraActions(aa ui.KeyActions) { aa[tcell.KeyCtrlB] = ui.NewKeyAction("Rollback", r.rollbackCmd, true) } -func (r *ReplicaSet) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := r.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -func (r *ReplicaSet) showPods(app *App, ns, res, sel string) { +func (r *ReplicaSet) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) s, err := k8s.NewReplicaSet(app.Conn()).Get(ns, n) if err != nil { @@ -174,7 +164,7 @@ func rollback(Conn k8s.Connection, selectedItem string) (string, error) { if err != nil { return "", err } - rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{apiGroup, kind}, Conn.DialOrDie()) + rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{Group: apiGroup, Kind: kind}, Conn.DialOrDie()) if err != nil { return "", err } diff --git a/internal/view/dump.go b/internal/view/screen_dump.go similarity index 84% rename from internal/view/dump.go rename to internal/view/screen_dump.go index d5c14403..475b1ba8 100644 --- a/internal/view/dump.go +++ b/internal/view/screen_dump.go @@ -35,24 +35,27 @@ type ScreenDump struct { app *App } -func NewScreenDump(title, _ string, _ resource.List) ResourceViewer { +func NewScreenDump(_, _ string, _ resource.List) ResourceViewer { return &ScreenDump{ - MasterDetail: NewMasterDetail(title), + MasterDetail: NewMasterDetail(dumpTitle, ""), } } // Init initializes the viewer. func (s *ScreenDump) Init(ctx context.Context) { s.app = ctx.Value(ui.KeyApp).(*App) + s.MasterDetail.Init(ctx) + s.registerActions() table := s.masterPage() - table.SetBorderFocusColor(tcell.ColorSteelBlue) - table.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) - table.SetColorerFn(dumpColorer) - table.SetActiveNS(resource.AllNamespaces) - table.SetSortCol(table.NameColIndex()+1, 0, true) - table.SelectRow(1, true) - + { + table.SetBorderFocusColor(tcell.ColorSteelBlue) + table.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) + table.SetColorerFn(dumpColorer) + table.SetActiveNS(resource.AllNamespaces) + table.SetSortCol(table.NameColIndex(), 0, true) + table.SelectRow(1, true) + } s.Start() s.refresh() } @@ -90,28 +93,18 @@ func (s *ScreenDump) refresh() { } func (s *ScreenDump) registerActions() { - aa := ui.KeyActions{ - ui.KeyP: ui.NewKeyAction("Previous", s.app.PrevCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Enter", s.enterCmd, true), + s.masterPage().AddActions(ui.KeyActions{ + tcell.KeyEsc: ui.NewKeyAction("Back", s.app.PrevCmd, false), + tcell.KeyEnter: ui.NewKeyAction("View", s.enterCmd, true), tcell.KeyCtrlD: ui.NewKeyAction("Delete", s.deleteCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", noopCmd, false), - } - s.masterPage().AddActions(aa) + }) } func (s *ScreenDump) getTitle() string { return dumpTitle } -func (s *ScreenDump) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - tv := s.masterPage() - tv.SetSortCol(tv.NameColIndex()+col, 0, asc) - tv.Refresh() - return nil - } -} - func (s *ScreenDump) enterCmd(evt *tcell.EventKey) *tcell.EventKey { log.Debug().Msg("Dump enter!") tv := s.masterPage() @@ -160,7 +153,14 @@ func (s *ScreenDump) backCmd(evt *tcell.EventKey) *tcell.EventKey { } func (s *ScreenDump) Hints() model.MenuHints { - return s.Hints() + if s.CurrentPage() == nil { + return nil + } + if c, ok := s.CurrentPage().Item.(model.Hinter); ok { + return c.Hints() + } + + return nil } func (s *ScreenDump) hydrate() resource.TableData { diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go new file mode 100644 index 00000000..a94448a5 --- /dev/null +++ b/internal/view/screen_dump_test.go @@ -0,0 +1,16 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestScreenDumpNew(t *testing.T) { + po := view.NewScreenDump("fred", "blee", nil) + po.Init(makeCtx()) + + assert.Equal(t, "Screen Dumps", po.Name()) + assert.Equal(t, 11, len(po.Hints())) +} diff --git a/internal/view/scroll_indicator.go b/internal/view/scroll_indicator.go new file mode 100644 index 00000000..f56003fc --- /dev/null +++ b/internal/view/scroll_indicator.go @@ -0,0 +1,57 @@ +package view + +import ( + "fmt" + "sync/atomic" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" +) + +// AutoScrollIndicator represents a log autoscroll status indicator. +type AutoScrollIndicator struct { + *tview.TextView + + styles *config.Styles + scrollStatus int32 +} + +// NewAutoScrollIndicator returns a new indicator. +func NewAutoScrollIndicator(styles *config.Styles) *AutoScrollIndicator { + a := AutoScrollIndicator{ + styles: styles, + TextView: tview.NewTextView(), + scrollStatus: 1, + } + a.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor)) + a.SetTextAlign(tview.AlignRight) + a.SetDynamicColors(true) + + return &a +} + +func (a *AutoScrollIndicator) AutoScroll() bool { + return atomic.LoadInt32(&a.scrollStatus) == 1 +} + +func (a *AutoScrollIndicator) ToggleAutoScroll() { + var val int32 = 1 + if a.AutoScroll() { + val = 0 + } + atomic.StoreInt32(&a.scrollStatus, val) +} + +func (a *AutoScrollIndicator) Refresh() { + autoScroll := "Off" + if a.AutoScroll() { + autoScroll = "On" + } + a.update("Autoscroll: " + autoScroll) +} + +func (a *AutoScrollIndicator) update(status string) { + a.Clear() + fg, bg := a.styles.Frame().Crumb.FgColor, a.styles.Frame().Crumb.ActiveColor + fmt.Fprintf(a, "[%s:%s:b] %-15s ", fg, bg, status) +} diff --git a/internal/view/scroll_indicator_test.go b/internal/view/scroll_indicator_test.go new file mode 100644 index 00000000..e47af271 --- /dev/null +++ b/internal/view/scroll_indicator_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestScrollIndicatorRefresg(t *testing.T) { + defaults, _ := config.NewStyles("") + v := view.NewAutoScrollIndicator(defaults) + v.Refresh() + + assert.Equal(t, "[black:orange:b] Autoscroll: On \n", v.GetText(false)) +} diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go new file mode 100644 index 00000000..11bd91ef --- /dev/null +++ b/internal/view/secret_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestSecretNew(t *testing.T) { + s := view.NewSecret("secrets", "", resource.NewSecretList(nil, "")) + s.Init(makeCtx()) + + assert.Equal(t, "secrets", s.Name()) + assert.Equal(t, 19, len(s.Hints())) +} diff --git a/internal/view/select_list.go b/internal/view/select_list.go index e10af0aa..e92f802c 100644 --- a/internal/view/select_list.go +++ b/internal/view/select_list.go @@ -13,11 +13,11 @@ import ( type selectList struct { *tview.List - parent loggable + parent Loggable actions ui.KeyActions } -func newSelectList(parent loggable) *selectList { +func newSelectList(parent Loggable) *selectList { v := selectList{List: tview.NewList(), actions: ui.KeyActions{}} { v.parent = parent diff --git a/internal/view/status.go b/internal/view/status.go deleted file mode 100644 index c71eb535..00000000 --- a/internal/view/status.go +++ /dev/null @@ -1,35 +0,0 @@ -package view - -import ( - "fmt" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" -) - -type statusView struct { - *tview.TextView - - styles *config.Styles -} - -func newStatusView(styles *config.Styles) *statusView { - v := statusView{styles: styles, TextView: tview.NewTextView()} - { - v.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor)) - v.SetTextAlign(tview.AlignRight) - v.SetDynamicColors(true) - } - return &v -} - -func (v *statusView) update(status []string) { - v.Clear() - last, bgColor := len(status)-1, v.styles.Frame().Crumb.BgColor - for i, c := range status { - if i == last { - bgColor = v.styles.Frame().Crumb.ActiveColor - } - fmt.Fprintf(v, "[%s:%s:b] %-15s ", v.styles.Frame().Crumb.FgColor, bgColor, c) - } -} diff --git a/internal/view/status_test.go b/internal/view/status_test.go deleted file mode 100644 index 188dea75..00000000 --- a/internal/view/status_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package view - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/stretchr/testify/assert" -) - -func TestNewStatus(t *testing.T) { - defaults, _ := config.NewStyles("") - v := newStatusView(defaults) - v.update([]string{"blee", "duh"}) - - assert.Equal(t, "[black:aqua:b] blee [black:orange:b] duh \n", v.GetText(false)) -} diff --git a/internal/view/sts.go b/internal/view/sts.go index d94bbb2f..9bc8ae82 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -36,7 +36,7 @@ func (s *StatefulSet) extraActions(aa ui.KeyActions) { aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", s.sortColCmd(2, false), false) } -func (s *StatefulSet) showPods(app *App, ns, res, sel string) { +func (s *StatefulSet) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) st, err := k8s.NewStatefulSet(app.Conn()).Get(ns, n) if err != nil { diff --git a/internal/view/styles.go b/internal/view/styles.go deleted file mode 100644 index 98ed8599..00000000 --- a/internal/view/styles.go +++ /dev/null @@ -1,46 +0,0 @@ -package view - -import ( - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -type styles struct { - color tcell.Color - attrs tcell.AttrMask - align int -} - -func stylesFor(app *App, res string, col int) styles { - switch res { - case "pod": - return podStyles(app, col) - default: - return defaultStyles(app, col) - } -} - -func podStyles(app *App, col int) styles { - st := styles{ - color: ui.StdColor, - attrs: tcell.AttrReverse, - align: tview.AlignLeft, - } - - switch col { - case 5, 6, 7, 8: - st.align = tview.AlignLeft - st.color = tcell.ColorGreen - } - - return st -} - -func defaultStyles(app *App, col int) styles { - return styles{ - color: tcell.ColorRed, - attrs: tcell.AttrReverse, - align: tview.AlignLeft, - } -} diff --git a/internal/view/subject.go b/internal/view/subject.go index a52155c0..061327f9 100644 --- a/internal/view/subject.go +++ b/internal/view/subject.go @@ -96,9 +96,8 @@ func (s *Subject) bindKeys() { s.AddActions(ui.KeyActions{ tcell.KeyEnter: ui.NewKeyAction("Policies", s.policyCmd, true), - tcell.KeyEscape: ui.NewKeyAction("Reset", s.resetCmd, false), + tcell.KeyEscape: ui.NewKeyAction("Back", s.resetCmd, false), ui.KeySlash: ui.NewKeyAction("Filter", s.activateCmd, false), - ui.KeyP: ui.NewKeyAction("Previous", s.app.PrevCmd, false), ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.SortColCmd(1), false), }) } diff --git a/internal/view/svc.go b/internal/view/svc.go index 9012dfb9..cb712027 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -1,6 +1,7 @@ package view import ( + "context" "errors" "fmt" "strings" @@ -16,6 +17,7 @@ import ( v1 "k8s.io/api/core/v1" ) +// Service represents a service viewer. type Service struct { *Resource @@ -23,15 +25,21 @@ type Service struct { logs *Logs } +// NewService returns a new viewer. func NewService(title, gvr string, list resource.List) ResourceViewer { - s := Service{ + return &Service{ Resource: NewResource(title, gvr, list), } +} + +// Init initializes the viewer. +func (s *Service) Init(ctx context.Context) { s.extraActionsFn = s.extraActions s.enterFn = s.showPods - s.logs = NewLogs(list.GetName(), &s) + s.Resource.Init(ctx) - return &s + s.logs = NewLogs(s.list.GetName(), s) + s.logs.Init(ctx) } // Protocol... @@ -51,17 +59,7 @@ func (s *Service) extraActions(aa ui.KeyActions) { aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", s.sortColCmd(1, false), false) } -func (s *Service) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := s.masterPage() - t.SetSortCol(t.NameColIndex()+col, 0, asc) - t.Refresh() - - return nil - } -} - -func (s *Service) showPods(app *App, ns, res, sel string) { +func (s *Service) showPods(app *App, _, res, sel string) { ns, n := namespaced(sel) svc, err := k8s.NewService(app.Conn()).Get(ns, n) if err != nil { @@ -79,8 +77,7 @@ func (s *Service) logsCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - l := s.GetPrimitive("logs").(*Logs) - l.reload("", s, false) + s.logs.reload("", s, false) s.Push(s.logs) return nil @@ -177,6 +174,10 @@ func (s *Service) benchCmd(evt *tcell.EventKey) *tcell.EventKey { } func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error { + if cfg.HTTP.Host == "" { + return fmt.Errorf("Invalid benchmark host %q", cfg.HTTP.Host) + } + var err error base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path if s.bench, err = perf.NewBenchmark(base, cfg); err != nil { diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go new file mode 100644 index 00000000..45855da9 --- /dev/null +++ b/internal/view/svc_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestServiceNew(t *testing.T) { + s := view.NewService("service", "", resource.NewServiceList(nil, "")) + s.Init(makeCtx()) + + assert.Equal(t, "svc", s.Name()) + assert.Equal(t, 22, len(s.Hints())) +} diff --git a/internal/view/table.go b/internal/view/table.go index 08a0b384..a7444776 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -5,6 +5,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" ) type Table struct { @@ -21,6 +22,8 @@ func NewTable(title string) *Table { } func (t *Table) Init(ctx context.Context) { + log.Debug().Msgf("VIEW Table INIT %q", t.GetBaseTitle()) + t.app = ctx.Value(ui.KeyApp).(*App) ctx = context.WithValue(ctx, ui.KeyStyles, t.app.Styles) @@ -102,12 +105,15 @@ func (t *Table) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { } func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !t.SearchBuff().Empty() { - t.app.Flash().Info("Clearing filter...") + log.Debug().Msgf("Table filter reset!") + if t.SearchBuff().Empty() { + return evt } + if ui.IsLabelSelector(t.SearchBuff().String()) { t.filterFn("") } + t.app.Flash().Info("Clearing filter...") t.SearchBuff().Reset() t.Refresh() @@ -115,6 +121,7 @@ func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("Table filter activated!") if t.app.InCmdMode() { return evt } diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index 0bd203bd..554f2228 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -45,9 +45,13 @@ func saveTable(cluster, name string, data resource.TableData) (string, error) { } w := csv.NewWriter(file) - w.Write(data.Header) + if err := w.Write(data.Header); err != nil { + return "", err + } for _, r := range data.Rows { - w.Write(r.Fields) + if err := w.Write(r.Fields); err != nil { + return "", err + } } w.Flush() if err := w.Error(); err != nil { @@ -67,53 +71,3 @@ func skinTitle(fmat string, style config.Frame) string { return fmat } - -func sortRows(evts resource.RowEvents, sortFn ui.SortFn, sortCol ui.SortColumn, keys []string) { - rows := make(resource.Rows, 0, len(evts)) - for k, r := range evts { - rows = append(rows, append(r.Fields, k)) - } - sortFn(rows, sortCol) - - for i, r := range rows { - keys[i] = r[len(r)-1] - } -} - -// func defaultSort(rows resource.Rows, sortCol ui.SortColumn) { -// t := rowSorter{rows: rows, index: sortCol.index, asc: sortCol.asc} -// sort.Sort(t) -// } - -// func sortAllRows(col ui.SortColumn, rows resource.RowEvents, sortFn ui.SortFn) (resource.Row, map[string]resource.Row) { -// keys := make([]string, len(rows)) -// sortRows(rows, sortFn, col, keys) - -// sec := make(map[string]resource.Row, len(rows)) -// for _, k := range keys { -// grp := rows[k].Fields[col.index] -// sec[grp] = append(sec[grp], k) -// } - -// // Performs secondary to sort by name for each groups. -// prim := make(resource.Row, 0, len(sec)) -// for k, v := range sec { -// sort.Strings(v) -// prim = append(prim, k) -// } -// sort.Sort(groupSorter{prim, col.asc}) - -// return prim, sec -// } - -// func sortIndicator(col ui.SortColumn, style config.Table, index int, name string) string { -// if col.index != index { -// return name -// } - -// order := descIndicator -// if col.asc { -// order = ascIndicator -// } -// return fmt.Sprintf("%s[%s::]%s[::]", name, style.Header.SorterColor, order) -// } diff --git a/internal/view/yaml.go b/internal/view/yaml.go index a806ad5f..85a3493b 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -78,7 +78,7 @@ func saveYAML(cluster, name, data string) (string, error) { log.Error().Err(err).Msgf("YAML create %s", path) return "", nil } - if _, err := fmt.Fprintf(file, data); err != nil { + if _, err := file.Write([]byte(data)); err != nil { return "", err } diff --git a/internal/watch/informer.go b/internal/watch/informer.go index d7f0f55a..e20bd0a3 100644 --- a/internal/watch/informer.go +++ b/internal/watch/informer.go @@ -131,7 +131,6 @@ func (i *Informer) List(res, ns string, opts metav1.ListOptions) (k8s.Collection // Get a resource by name. func (i *Informer) Get(res, fqn string, opts metav1.GetOptions) (interface{}, error) { if i == nil { - panic("blee") return nil, errors.New("Invalid Get informer") }