diff --git a/README.md b/README.md index 82932b22..c828e36f 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,8 @@ K9s uses aliases to navigate most K8s resources. | Command | Result | Example | |-----------------------|----------------------------------------------------|----------------------------| | `:`alias`` | View a Kubernetes resource | `:po` | -| '?' | Show all command aliases | select+ to view | +| `?` | Show keyboard shortcuts and help | | +| `A` | Show all available resource alias | select+`` to view | | `/`filter`ENTER`> | Filter out a resource view given a filter | `/bumblebeetuna` | | `` | Bails out of command mode | | | `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) | diff --git a/change_logs/release_0.4.5.md b/change_logs/release_0.4.5.md new file mode 100644 index 00000000..4e3c6b5d --- /dev/null +++ b/change_logs/release_0.4.5.md @@ -0,0 +1,42 @@ + + +# Release v0.4.5 + +## Notes + +Thank you to all that contributed with flushing out issues with K9s! I'll try +to mark some of these issues as fixed. But if you don't mind grab the latest +rev and see if we're happier with some of the fixes! + +If you've filed an issue please help me verify and close. + +Thank you so much for your support and awesome suggestions to make K9s better!! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + +### Multi containers + + There was an [issue](https://github.com/derailed/k9s/issues/135) where we ran into limitations with the container + selection keyboard shortcuts only allowing up to 10 containers. In this release, we've changed to a pick list vs the menu + to select containers for both shell and logs access. This gives K9s the ability to select up to 26 containers now. This + is not in any way an *encouragement* to have so many containers per pods!! + +### Alias View ShortCut + + The change above entailed having to move the alias shortcut to `A` vs `a` as the pick list shortcuts conflicted with + the alias view keyboard activation. + + +--- + +## Resolved Bugs + ++ [Issue #152](https://github.com/derailed/k9s/issues/152) + +--- + + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/config/config.go b/internal/config/config.go index 97ae0e9c..04fad978 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -114,7 +114,11 @@ func (c *Config) ActiveNamespace() string { // FavNamespaces returns fav namespaces in the current cluster. func (c *Config) FavNamespaces() []string { - return c.K9s.ActiveCluster().Namespace.Favorites + cl := c.K9s.ActiveCluster() + if cl != nil { + return c.K9s.ActiveCluster().Namespace.Favorites + } + return []string{} } // SetActiveNamespace set the active namespace in the current cluster. @@ -136,7 +140,10 @@ func (c *Config) ActiveView() string { // SetActiveView set the currently cluster active view func (c *Config) SetActiveView(view string) { - c.K9s.ActiveCluster().View.Active = view + cl := c.K9s.ActiveCluster() + if cl != nil { + cl.View.Active = view + } } // GetConnection return an api server connection. diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 04f67bdc..9ac4809b 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -35,14 +35,13 @@ func (k *K9s) ActiveCluster() *Cluster { if k.Clusters == nil { k.Clusters = map[string]*Cluster{} } - if len(k.CurrentCluster) == 0 { - return nil - } if c, ok := k.Clusters[k.CurrentCluster]; ok { return c } + k.Clusters[k.CurrentCluster] = NewCluster() + return k.Clusters[k.CurrentCluster] } diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index 819194db..8b855cbf 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -60,7 +60,7 @@ func TestK9sActiveClusterZero(t *testing.T) { func TestK9sActiveClusterBlank(t *testing.T) { var c config.K9s cl := c.ActiveCluster() - assert.Nil(t, cl) + assert.Equal(t, config.NewCluster(), cl) } func TestK9sActiveCluster(t *testing.T) { diff --git a/internal/k8s/api.go b/internal/k8s/api.go index aaa7d4d3..558f90fa 100644 --- a/internal/k8s/api.go +++ b/internal/k8s/api.go @@ -250,6 +250,7 @@ func (a *APIClient) supportsMxServer() bool { func (a *APIClient) SupportsRes(group string, versions []string) (string, bool) { apiGroups, err := a.DialOrDie().Discovery().ServerGroups() if err != nil { + log.Error().Err(err).Msg("Unable to dial api groups") return "", false } diff --git a/internal/views/app.go b/internal/views/app.go index 2131e60f..5342d03c 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -87,7 +87,7 @@ func NewApp(cfg *config.Config) *appView { v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd, false) v.actions[tcell.KeyCtrlC] = newKeyAction("Quit", v.quitCmd, false) v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd, false) - v.actions[KeyA] = newKeyAction("Aliases", v.aliasCmd, true) + v.actions[KeyShiftA] = newKeyAction("Aliases", v.aliasCmd, true) v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd, false) v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, false) v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) diff --git a/internal/views/job.go b/internal/views/job.go index bf148060..be2047ca 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -14,10 +14,18 @@ func newJobView(t string, app *appView, list resource.List) resourceViewer { v := jobView{newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions - v.AddPage("logs", newLogsView(&v), true, false) + v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) v.switchPage("job") } + picker := newSelectList(&v) + { + picker.setActions(keyActions{ + tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true}, + }) + } + v.AddPage("picker", picker, true, false) + return &v } @@ -41,7 +49,7 @@ func (v *jobView) getSelection() string { // Handlers... -func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey { +func (v *jobView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { return evt } @@ -49,22 +57,35 @@ func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey { cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { v.app.flash(flashErr, err.Error()) - log.Error().Err(err) + log.Error().Err(err).Msgf("Unable to fetch containers for %s", v.selectedItem) return evt } - l := v.GetPrimitive("logs").(*logsView) - l.deleteAllPages() - for _, c := range cc { - l.addContainer(c) + if len(cc) == 1 { + v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v) + return nil } - v.switchPage("logs") - l.init() + picker := v.GetPrimitive("picker").(*selectList) + picker.populate(cc) + picker.SetSelectedFunc(func(i int, t, d string, r rune) { + v.showLogs(v.selectedItem, t, "picker", picker) + }) + v.switchPage("picker") return nil } -func (v *jobView) extraActions(aa keyActions) { - aa[KeyL] = newKeyAction("Logs", v.logs, true) +func (v *jobView) showLogs(path, co, view string, parent loggable) { + l := v.GetPrimitive("logs").(*logsView) + l.parent = parent + l.parentView = view + l.deleteAllPages() + l.addContainer(co) + v.switchPage("logs") + l.init() +} + +func (v *jobView) extraActions(aa keyActions) { + aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) } diff --git a/internal/views/log.go b/internal/views/log.go index 00694bff..1117f68d 100644 --- a/internal/views/log.go +++ b/internal/views/log.go @@ -9,6 +9,7 @@ import ( type logView struct { *detailsView + ansiWriter io.Writer } @@ -21,13 +22,15 @@ func newLogView(title string, parent loggable) *logView { v.SetWrap(true) v.setTitle(parent.getSelection()) v.SetMaxBuffer(parent.appView().config.K9s.LogBufferSize) + v.ansiWriter = tview.ANSIWriter(v) } - v.ansiWriter = tview.ANSIWriter(v) + return &v } func (l *logView) logLine(line string) { fmt.Fprintln(l.ansiWriter, tview.Escape(line)) + l.ScrollToEnd() } func (l *logView) log(lines fmt.Stringer) { diff --git a/internal/views/logs.go b/internal/views/logs.go index cd82c216..25b85142 100644 --- a/internal/views/logs.go +++ b/internal/views/logs.go @@ -21,16 +21,18 @@ const ( type logsView struct { *tview.Pages + parentView string parent loggable containers []string actions keyActions cancelFunc context.CancelFunc } -func newLogsView(parent loggable) *logsView { +func newLogsView(pview string, parent loggable) *logsView { v := logsView{ Pages: tview.NewPages(), parent: parent, + parentView: pview, containers: []string{}, } v.setActions(keyActions{ @@ -93,6 +95,7 @@ func (v *logsView) hints() hints { v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil, true) } } + return v.actions.toHints() } @@ -169,6 +172,7 @@ func (v *logsView) doLoad(path, co string) error { return err } v.cancelFunc = cancelFn + return nil } @@ -177,7 +181,8 @@ func (v *logsView) doLoad(path, co string) error { func (v *logsView) back(evt *tcell.EventKey) *tcell.EventKey { v.stop() - v.parent.switchPage(v.parent.getList().GetName()) + v.parent.switchPage(v.parentView) + return nil } @@ -186,6 +191,7 @@ func (v *logsView) top(evt *tcell.EventKey) *tcell.EventKey { v.parent.appView().flash(flashInfo, "Top of logs...") p.Item.(*logView).ScrollToBeginning() } + return nil } @@ -194,6 +200,7 @@ func (v *logsView) bottom(*tcell.EventKey) *tcell.EventKey { v.parent.appView().flash(flashInfo, "Bottom of logs...") p.Item.(*logView).ScrollToEnd() } + return nil } @@ -203,6 +210,7 @@ func (v *logsView) pageUp(*tcell.EventKey) *tcell.EventKey { v.parent.appView().flash(flashInfo, "Reached Top ...") } } + return nil } @@ -212,6 +220,7 @@ func (v *logsView) pageDown(*tcell.EventKey) *tcell.EventKey { v.parent.appView().flash(flashInfo, "Reached Bottom ...") } } + return nil } @@ -220,5 +229,6 @@ func (v *logsView) clearLogs(*tcell.EventKey) *tcell.EventKey { v.parent.appView().flash(flashInfo, "Clearing logs...") p.Item.(*logView).Clear() } + return nil } diff --git a/internal/views/pod.go b/internal/views/pod.go index 6a1cd2e1..e21a3bde 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -1,8 +1,6 @@ package views import ( - "fmt" - "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -26,16 +24,17 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer { v.extraActionsFn = v.extraActions } - v.AddPage("logs", newLogsView(&v), true, false) - - picker := newSelectList() + picker := newSelectList(&v) { picker.setActions(keyActions{ - tcell.KeyEscape: {description: "Back", action: v.backCmd}, + tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true}, }) - v.AddPage("choose", picker, true, false) } + v.AddPage("picker", picker, true, false) + + v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) v.switchPage("po") + return &v } @@ -63,49 +62,64 @@ func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { return evt } + cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { v.app.flash(flashErr, err.Error()) log.Error().Err(err) return evt } - l := v.GetPrimitive("logs").(*logsView) - l.deleteAllPages() - for _, c := range cc { - l.addContainer(c) + + if len(cc) == 1 { + v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v) + return nil } + + picker := v.GetPrimitive("picker").(*selectList) + picker.populate(cc) + picker.SetSelectedFunc(func(i int, t, d string, r rune) { + v.showLogs(v.selectedItem, t, "picker", picker) + }) + v.switchPage("picker") + + return nil +} + +func (v *podView) showLogs(path, co, view string, parent loggable) { + l := v.GetPrimitive("logs").(*logsView) + l.parent = parent + l.parentView = view + l.deleteAllPages() + l.addContainer(co) v.switchPage("logs") l.init() - return nil } func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { return evt } + cc, err := fetchContainers(v.list, v.selectedItem, false) if err != nil { v.app.flash(flashErr, err.Error()) log.Error().Msgf("Error fetching containers %v", err) return evt } + if len(cc) == 1 { v.shellIn(v.selectedItem, "") - } else { - p := v.GetPrimitive("choose").(*selectList) - p.populate(cc) - p.SetSelectedFunc(func(i int, t, d string, r rune) { - v.shellIn(v.selectedItem, t) - }) - v.switchPage("choose") + return nil } - return evt -} -func (v *podView) showPicker(cc []string) { - l := v.GetPrimitive("choose").(*selectList) - l.populate(cc) - v.switchPage("choose") + p := v.GetPrimitive("picker").(*selectList) + p.populate(cc) + p.SetSelectedFunc(func(i int, t, d string, r rune) { + v.shellIn(v.selectedItem, t) + }) + v.switchPage("picker") + + return evt } func (v *podView) shellIn(path, co string) { @@ -123,22 +137,6 @@ func (v *podView) shellIn(path, co string) { runK(v.app, args...) } -func (v *podView) showLogs(path, co string, previous bool) { - ns, po := namespaced(path) - args := make([]string, 0, 10) - args = append(args, "logs", "-f") - args = append(args, "-n", ns) - args = append(args, "--context", v.app.config.K9s.CurrentContext) - if len(co) != 0 { - args = append(args, "-c", co) - v.app.flash(flashInfo, fmt.Sprintf("Viewing logs from container %s on pod %s", co, po)) - } else { - v.app.flash(flashInfo, fmt.Sprintf("Viewing logs from pod %s", po)) - } - args = append(args, po) - runK(v.app, args...) -} - func (v *podView) extraActions(aa keyActions) { aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 1752562e..3bfd075e 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -329,7 +329,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { listFn: resource.NewHPAList, } default: - log.Panic().Msgf("K9s does not currently support HPA version %s", rev) + log.Panic().Msgf("K9s does not currently support HPA version `%s`", rev) } return cmds diff --git a/internal/views/select_list.go b/internal/views/select_list.go index 73c3d2b2..03a28aa6 100644 --- a/internal/views/select_list.go +++ b/internal/views/select_list.go @@ -1,8 +1,7 @@ package views import ( - "strconv" - + "github.com/derailed/k9s/internal/resource" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -10,13 +9,19 @@ import ( type selectList struct { *tview.List + parent loggable actions keyActions } -func newSelectList() *selectList { - v := selectList{List: tview.NewList()} +func newSelectList(parent loggable) *selectList { + v := selectList{List: tview.NewList(), actions: keyActions{}} { + v.parent = parent v.SetBorder(true) + v.SetMainTextColor(tcell.ColorGray) + v.ShowSecondaryText(false) + v.SetShortcutColor(tcell.ColorAqua) + v.SetSelectedBackgroundColor(tcell.ColorAqua) v.SetTitle(" [aqua::b]Container Selector ") v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { if a, ok := v.actions[evt.Key()]; ok { @@ -26,9 +31,38 @@ func newSelectList() *selectList { return evt }) } + return &v } +func (v *selectList) back(evt *tcell.EventKey) *tcell.EventKey { + v.parent.switchPage(v.parent.getList().GetName()) + + return nil +} + +// Protocol... + +func (v *selectList) switchPage(p string) { + v.parent.switchPage(p) +} + +func (v *selectList) backFn() actionHandler { + return v.parent.backFn() +} + +func (v *selectList) appView() *appView { + return v.parent.appView() +} + +func (v *selectList) getList() resource.List { + return v.parent.getList() +} + +func (v *selectList) getSelection() string { + return v.parent.getSelection() +} + // SetActions to handle keyboard events. func (v *selectList) setActions(aa keyActions) { v.actions = aa @@ -38,12 +72,13 @@ func (v *selectList) hints() hints { if v.actions != nil { return v.actions.toHints() } + return nil } func (v *selectList) populate(ss []string) { v.Clear() for i, s := range ss { - v.AddItem(s, "Select a container", rune(strconv.Itoa(i)[0]), nil) + v.AddItem(s, "Select a container", rune('a'+i), nil) } }