diff --git a/change_logs/release_0.7.3.md b/change_logs/release_0.7.3.md new file mode 100644 index 00000000..96ee0c7e --- /dev/null +++ b/change_logs/release_0.7.3.md @@ -0,0 +1,27 @@ + + +# Release v0.7.3 + +## 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. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + + +--- + +## Resolved Bugs/Features + ++ [Issue #210](https://github.com/derailed/k9s/issues/210) ++ [Issue #209](https://github.com/derailed/k9s/issues/209) ++ [Issue #206](https://github.com/derailed/k9s/issues/206) Thank you @carlowouters!! + + +--- + + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/config/style.go b/internal/config/style.go index 991ace97..95a2adb9 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -191,7 +191,7 @@ func newTable() *Table { func newTableHeader() *TableHeader { return &TableHeader{ FgColor: "white", - BgColor: "black", + BgColor: "red", SorterColor: "aqua", } } diff --git a/internal/k8s/resource.go b/internal/k8s/resource.go index 64e31790..0dca20e8 100644 --- a/internal/k8s/resource.go +++ b/internal/k8s/resource.go @@ -80,23 +80,6 @@ func (r *Resource) listAll(ns, n string) (runtime.Object, error) { Do().Get() } -func (r *Resource) getOne(ns, n string) (runtime.Object, error) { - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) - _, codec := r.codecs() - - c, err := r.getClient() - if err != nil { - return nil, err - } - - return c.Get(). - SetHeader("Accept", a). - Namespace(ns). - Resource(n). - VersionedParams(&metav1beta1.TableOptions{}, codec). - Do().Get() -} - func (r *Resource) getClient() (*rest.RESTClient, error) { crConfig := r.RestConfigOrDie() crConfig.GroupVersion = &schema.GroupVersion{Group: r.group, Version: r.version} diff --git a/internal/resource/container.go b/internal/resource/container.go index ae6eba95..1572ee5d 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -287,9 +287,9 @@ func toRes(r v1.ResourceList) (string, string) { func probe(p *v1.Probe) string { if p == nil { - return "on" + return "off" } - return "off" + return "on" } func asMi(v int64) float64 { diff --git a/internal/resource/container_test.go b/internal/resource/container_test.go new file mode 100644 index 00000000..9274e6bf --- /dev/null +++ b/internal/resource/container_test.go @@ -0,0 +1,110 @@ +package resource + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestProbe(t *testing.T) { + uu := map[string]struct { + probe *v1.Probe + e string + }{ + "defined": {&v1.Probe{}, "on"}, + "undefined": {nil, "off"}, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, probe(u.probe)) + }) + } +} + +func TestAsMi(t *testing.T) { + uu := map[string]struct { + mem int64 + e float64 + }{ + "zero": {0, 0}, + "1Mb": {1024 * 1024, 1.048576e+06}, + "10Mb": {10 * 1024 * 1024, 1.048576e+07}, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, asMi(u.mem)) + }) + } +} + +func TestToRes(t *testing.T) { + uu := map[string]struct { + res v1.ResourceList + ecpu, emem string + }{ + "cool": {v1.ResourceList{ + v1.ResourceCPU: toQty("10m"), + v1.ResourceMemory: toQty("20Mi"), + }, + "10", "20"}, + "noRes": {v1.ResourceList{}, + "0", "0"}, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + cpu, mem := toRes(u.res) + assert.Equal(t, u.ecpu, cpu) + assert.Equal(t, u.emem, mem) + }) + } +} + +func TestToState(t *testing.T) { + uu := map[string]struct { + state v1.ContainerState + e string + }{ + "empty": {v1.ContainerState{}, + MissingValue}, + "running": { + v1.ContainerState{Running: &v1.ContainerStateRunning{}}, + "Running", + }, + "waiting": { + v1.ContainerState{Waiting: &v1.ContainerStateWaiting{}}, + "Waiting", + }, + "waitingReason": { + v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "blee"}}, + "blee", + }, + "terminated": { + v1.ContainerState{Terminated: &v1.ContainerStateTerminated{}}, + "Terminated", + }, + "terminatedReason": { + v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "blee"}}, + "blee", + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, toState(u.state)) + }) + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toQty(s string) resource.Quantity { + q, _ := resource.ParseQuantity(s) + + return q +} diff --git a/internal/resource/custom.go b/internal/resource/custom.go index 0bcc72fb..0004cd2b 100644 --- a/internal/resource/custom.go +++ b/internal/resource/custom.go @@ -94,7 +94,7 @@ func (r *Custom) List(ns string) (Columnars, error) { return nil, err } - if len(ii) != 1 { + if len(ii) == 0 { return Columnars{}, errors.New("no resources found") } diff --git a/internal/views/container.go b/internal/views/container.go index 55511bcc..d1155437 100644 --- a/internal/views/container.go +++ b/internal/views/container.go @@ -25,6 +25,7 @@ func newContainerView(t string, app *appView, list resource.List, path string, e { v.path = &path v.extraActionsFn = v.extraActions + v.enterFn = v.viewLogs v.colorerFn = containerColorer v.current = app.content.GetPrimitive("main").(igniter) v.exitFn = exitFn @@ -45,7 +46,6 @@ func (v *containerView) extraActions(aa keyActions) { aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false) aa[KeyP] = newKeyAction("Previous", v.backCmd, false) - aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false) aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true) aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true) aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(8, false), true) @@ -72,18 +72,22 @@ func (v *containerView) getSelection() string { // Handlers... +func (v *containerView) viewLogs(app *appView, _, res, sel string) { + cell := v.getTV().GetCell(v.selectedRow, 3) + if cell != nil && strings.TrimSpace(cell.Text) != "Running" { + v.app.flash().err(errors.New("No logs for a non running container")) + return + } + + v.showLogs(sel, v.list.GetName(), v, false) +} + func (v *containerView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { return evt } - cell := v.getTV().GetCell(v.selectedRow, 3) - if cell != nil && strings.TrimSpace(cell.Text) != "Running" { - v.app.flash().err(errors.New("No logs for a non running container")) - return evt - } - v.showLogs(v.selectedItem, v.list.GetName(), v, false) - + v.viewLogs(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem) return nil } diff --git a/internal/views/context.go b/internal/views/context.go index 66682a0b..74031646 100644 --- a/internal/views/context.go +++ b/internal/views/context.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/derailed/k9s/internal/resource" - "github.com/gdamore/tcell" ) type contextView struct { @@ -14,6 +13,7 @@ type contextView struct { func newContextView(t string, app *appView, list resource.List) resourceViewer { v := contextView{newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.useCtx v.getTV().cleanseFn = v.cleanser return &v @@ -21,21 +21,14 @@ func newContextView(t string, app *appView, list resource.List) resourceViewer { func (v *contextView) extraActions(aa keyActions) { delete(v.getTV().actions, KeyShiftA) - aa[tcell.KeyEnter] = newKeyAction("Switch", v.useCmd, true) } -func (v *contextView) useCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt +func (v *contextView) useCtx(app *appView, _, res, sel string) { + if err := v.useContext(sel); err != nil { + app.flash().err(err) + return } - if err := v.useContext(v.selectedItem); err != nil { - v.app.flash().err(err) - return evt - } - - v.app.gotoResource("po", true) - - return nil + app.gotoResource("po", true) } func (*contextView) cleanser(s string) string { diff --git a/internal/views/dp.go b/internal/views/dp.go index e8dc88be..c2a444c2 100644 --- a/internal/views/dp.go +++ b/internal/views/dp.go @@ -16,6 +16,7 @@ type deployView struct { func newDeployView(t string, app *appView, list resource.List) resourceViewer { v := deployView{newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods return &v } @@ -23,7 +24,6 @@ func newDeployView(t string, app *appView, list resource.List) resourceViewer { func (v *deployView) extraActions(aa keyActions) { aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *deployView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -36,30 +36,25 @@ func (v *deployView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tc } } -func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - ns, n := namespaced(v.selectedItem) - d := k8s.NewDeployment(v.app.conn()) +func (v *deployView) showPods(app *appView, _, res, sel string) { + ns, n := namespaced(sel) + d := k8s.NewDeployment(app.conn()) dep, err := d.Get(ns, n) if err != nil { - log.Error().Err(err).Msgf("Fetching Deployment %s", v.selectedItem) - v.app.flash().err(err) - return evt + log.Error().Err(err).Msgf("Fetching Deployment %s", sel) + app.flash().err(err) + return } + dp := dep.(*v1.Deployment) - - sel, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector) + l, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector) if err != nil { - log.Error().Err(err).Msgf("Converting selector for Deployment %s", v.selectedItem) - v.app.flash().err(err) - return evt + log.Error().Err(err).Msgf("Converting selector for Deployment %s", sel) + app.flash().err(err) + return } - showPods(v.app, ns, "Deployment", v.selectedItem, sel.String(), "", v.backCmd) - return nil + showPods(app, ns, "Deployment", sel, l.String(), "", v.backCmd) } func (v *deployView) backCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/ds.go b/internal/views/ds.go index 6e81adb8..0fde0627 100644 --- a/internal/views/ds.go +++ b/internal/views/ds.go @@ -16,6 +16,7 @@ type daemonSetView struct { func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer { v := daemonSetView{newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods return &v } @@ -23,7 +24,6 @@ func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer func (v *daemonSetView) extraActions(aa keyActions) { aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *daemonSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -36,30 +36,25 @@ func (v *daemonSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) } } -func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - ns, n := namespaced(v.selectedItem) - d := k8s.NewDaemonSet(v.app.conn()) +func (v *daemonSetView) showPods(app *appView, _, res, sel string) { + ns, n := namespaced(sel) + d := k8s.NewDaemonSet(app.conn()) dset, err := d.Get(ns, n) if err != nil { - log.Error().Err(err).Msgf("Fetching DeaemonSet %s", v.selectedItem) + log.Error().Err(err).Msgf("Fetching DeaemonSet %s", sel) v.app.flash().err(err) - return evt + return } + ds := dset.(*extv1beta1.DaemonSet) - - sel, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) + l, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) if err != nil { - log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", v.selectedItem) - v.app.flash().err(err) - return evt + log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", sel) + app.flash().err(err) + return } - showPods(v.app, ns, "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd) - return nil + showPods(app, ns, "DaemonSet", sel, l.String(), "", v.backCmd) } func (v *daemonSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/dump.go b/internal/views/dump.go index 08d14fa1..7eb3e33a 100644 --- a/internal/views/dump.go +++ b/internal/views/dump.go @@ -92,6 +92,7 @@ func (v *dumpView) registerActions() { v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, true) v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) + v.actions[tcell.KeyCtrlS] = newKeyAction("Save", v.app.noopCmd, false) vu := v.getTV() vu.setActions(v.actions) diff --git a/internal/views/helpers_test.go b/internal/views/helpers_test.go index 93149f03..6f5bd487 100644 --- a/internal/views/helpers_test.go +++ b/internal/views/helpers_test.go @@ -1,7 +1,6 @@ package views import ( - "fmt" "testing" "github.com/derailed/k9s/internal/config" @@ -106,7 +105,6 @@ func TestStripPort(t *testing.T) { for k, u := range uu { t.Run(k, func(t *testing.T) { - fmt.Println("TCP?", u.port, isTCPPort(u.port)) assert.Equal(t, u.e, stripPort(u.port)) }) } diff --git a/internal/views/job.go b/internal/views/job.go index 41e7f40b..58385b0e 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -16,6 +16,7 @@ type jobView struct { func newJobView(t string, app *appView, list resource.List) resourceViewer { v := jobView{resourceView: newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) picker := newSelectList(&v) @@ -99,33 +100,27 @@ func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) { func (v *jobView) extraActions(aa keyActions) { aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } -func (v *jobView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - ns, n := namespaced(v.selectedItem) - j := k8s.NewJob(v.app.conn()) +func (v *jobView) showPods(app *appView, _, res, sel string) { + ns, n := namespaced(sel) + j := k8s.NewJob(app.conn()) job, err := j.Get(ns, n) if err != nil { - log.Error().Err(err).Msgf("Fetching Job %s", v.selectedItem) - v.app.flash().err(err) - return evt + log.Error().Err(err).Msgf("Fetching Job %s", sel) + app.flash().err(err) + return } + jo := job.(*batchv1.Job) - - sel, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector) + l, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector) if err != nil { - log.Error().Err(err).Msgf("Converting selector for Job %s", v.selectedItem) - v.app.flash().err(err) - return evt + log.Error().Err(err).Msgf("Converting selector for Job %s", sel) + app.flash().err(err) + return } - showPods(v.app, "", "Job", v.selectedItem, sel.String(), "", v.backCmd) - return nil + showPods(app, "", "Job", sel, l.String(), "", v.backCmd) } func (v *jobView) backCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/no.go b/internal/views/no.go index 42f666b9..5d22907b 100644 --- a/internal/views/no.go +++ b/internal/views/no.go @@ -12,6 +12,7 @@ type nodeView struct { func newNodeView(t string, app *appView, list resource.List) resourceViewer { v := nodeView{newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods return &v } @@ -19,7 +20,6 @@ func newNodeView(t string, app *appView, list resource.List) resourceViewer { func (v *nodeView) extraActions(aa keyActions) { aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true) aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -32,14 +32,8 @@ func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcel } } -func (v *nodeView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - showPods(v.app, "", "Node", v.selectedItem, "", "spec.nodeName="+v.selectedItem, v.backCmd) - - return nil +func (v *nodeView) showPods(app *appView, _, res, sel string) { + showPods(app, "", "Node", sel, "", "spec.nodeName="+sel, v.backCmd) } func (v *nodeView) backCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/ns.go b/internal/views/ns.go index 6d4f2c9a..5525d48f 100644 --- a/internal/views/ns.go +++ b/internal/views/ns.go @@ -25,24 +25,19 @@ func newNamespaceView(t string, app *appView, list resource.List) resourceViewer v.extraActionsFn = v.extraActions v.selectedFn = v.getSelectedItem v.decorateFn = v.decorate + v.enterFn = v.switchNs v.getTV().cleanseFn = v.cleanser return &v } func (v *namespaceView) extraActions(aa keyActions) { - aa[tcell.KeyEnter] = newKeyAction("Switch", v.switchNsCmd, true) aa[KeyU] = newKeyAction("Use", v.useNsCmd, true) } -func (v *namespaceView) switchNsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - v.useNamespace(v.getSelectedItem()) - v.app.gotoResource("po", true) - - return nil +func (v *namespaceView) switchNs(app *appView, _, res, sel string) { + v.useNamespace(sel) + app.gotoResource("po", true) } func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/padding.go b/internal/views/padding.go index e196d11d..cd0fc313 100644 --- a/internal/views/padding.go +++ b/internal/views/padding.go @@ -22,7 +22,7 @@ func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) { } var row int - for k, rev := range table.Rows { + for _, rev := range table.Rows { ageIndex := len(rev.Fields) - 1 for index, field := range rev.Fields { // Date field comes out as timestamp. @@ -31,7 +31,6 @@ func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) { if err == nil { field = duration.HumanDuration(dur) } - table.Rows[k].Fields[index] = field } width := len(field) + colPadding if width > pads[index] { diff --git a/internal/views/pod.go b/internal/views/pod.go index 8c9adb06..75295240 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -64,20 +64,17 @@ func (v *podView) extraActions(aa keyActions) { } func (v *podView) listContainers(app *appView, _, res, sel string) { - if !v.rowSelected() { - return - } - po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{}) if err != nil { log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel) app.flash().errf("Unable to retrieve pods %s", err) return } + pod := po.(*v1.Pod) mx := k8s.NewMetricsServer(app.conn()) list := resource.NewContainerList(app.conn(), mx, pod) - title := skinTitle(fmt.Sprintf(containerFmt, "Containers", sel), v.app.styles.Style) + title := skinTitle(fmt.Sprintf(containerFmt, "Containers", sel), app.styles.Style) // Stop my updater if v.cancelFn != nil { diff --git a/internal/views/port_selector.go b/internal/views/port_selector.go new file mode 100644 index 00000000..f22c0485 --- /dev/null +++ b/internal/views/port_selector.go @@ -0,0 +1,43 @@ +package views + +import ( + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +type portSelector struct { + title, port string + 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 *appView) { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). + SetButtonTextColor(tview.Styles.PrimaryTextColor). + SetLabelColor(tcell.ColorAqua). + SetFieldTextColor(tcell.ColorOrange) + + f1 := p.port + f.AddInputField("Pod Port:", f1, 20, nil, func(changed string) { + f1 = changed + }) + + f.AddButton("OK", p.ok) + f.AddButton("Cancel", p.cancel) + + modal := tview.NewModalForm("<"+p.title+">", f) + modal.SetDoneFunc(func(_ int, b string) { + p.cancel() + }) +} diff --git a/internal/views/resource.go b/internal/views/resource.go index 6ef428ea..eed0809c 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -185,6 +185,11 @@ func (v *resourceView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { if v.getTV().filterCmd(evt) == nil { return nil } + + if v.selectedItem == "" { + return nil + } + if v.enterFn != nil { v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem) } else { diff --git a/internal/views/rs.go b/internal/views/rs.go index 7438c17f..36e51f2b 100644 --- a/internal/views/rs.go +++ b/internal/views/rs.go @@ -23,6 +23,7 @@ type replicaSetView struct { func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer { v := replicaSetView{newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods return &v } @@ -31,7 +32,6 @@ func (v *replicaSetView) extraActions(aa keyActions) { aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) aa[tcell.KeyCtrlB] = newKeyAction("Rollback", v.rollbackCmd, true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *replicaSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -44,30 +44,24 @@ func (v *replicaSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) } } -func (v *replicaSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - ns, n := namespaced(v.selectedItem) - rset := k8s.NewReplicaSet(v.app.conn()) +func (v *replicaSetView) showPods(app *appView, ns, res, sel string) { + ns, n := namespaced(sel) + rset := k8s.NewReplicaSet(app.conn()) r, err := rset.Get(ns, n) if err != nil { - log.Error().Err(err).Msgf("Fetching ReplicaSet %s", v.selectedItem) - v.app.flash().errf("Replicaset failed %s", err) - return evt + log.Error().Err(err).Msgf("Fetching ReplicaSet %s", sel) + app.flash().errf("Replicaset failed %s", err) } + rs := r.(*v1.ReplicaSet) - - sel, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) + l, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) if err != nil { - log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", v.selectedItem) - v.app.flash().errf("Selector failed %s", err) - return evt + log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", sel) + app.flash().errf("Selector failed %s", err) + return } - showPods(v.app, "", "ReplicaSet", v.selectedItem, sel.String(), "", v.backCmd) - return nil + showPods(app, "", "ReplicaSet", sel, l.String(), "", v.backCmd) } func (v *replicaSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/sorter.go b/internal/views/sorter.go index 380d07c5..3be693bd 100644 --- a/internal/views/sorter.go +++ b/internal/views/sorter.go @@ -70,9 +70,9 @@ func less(asc bool, c1, c2 string) bool { return !b } -func isDurationSort(asc bool, c1, c2 string) (bool, bool) { - d1, ok1 := isDuration(c1) - d2, ok2 := isDuration(c2) +func isDurationSort(asc bool, s1, s2 string) (bool, bool) { + d1, ok1 := isDuration(s1) + d2, ok2 := isDuration(s2) if !ok1 || !ok2 { return false, false } @@ -80,7 +80,7 @@ func isDurationSort(asc bool, c1, c2 string) (bool, bool) { if asc { return d1 <= d2, true } - return d1 > d2, true + return d1 >= d2, true } func isMetricSort(asc bool, c1, c2 string) (bool, bool) { diff --git a/internal/views/sorter_test.go b/internal/views/sorter_test.go index 0d355aaf..5d28f870 100644 --- a/internal/views/sorter_test.go +++ b/internal/views/sorter_test.go @@ -33,6 +33,7 @@ func TestGroupSort(t *testing.T) { {true, []string{"b-21", "b-2"}, []string{"b-2", "b-21"}}, {false, []string{"b-21", "b-2"}, []string{"b-21", "b-2"}}, {true, []string{"4m", "3m2s"}, []string{"3m2s", "4m"}}, + {true, []string{"3y37d", "2y4d"}, []string{"2y4d", "3y37d"}}, } for _, u := range uu { @@ -80,3 +81,24 @@ func TestRowSort(t *testing.T) { assert.Equal(t, u.expect, r.rows) } } + +func TestIsDurationSort(t *testing.T) { + uu := map[string]struct { + s1, s2 string + asc, e bool + }{ + "ascLess": {"10h5m", "2h10m", true, false}, + "descGreater": {"10h5m", "2h10m", false, true}, + "ascEqual": {"2h10m", "2h10m", true, true}, + "descEqual": {"2h10m", "2h10m", false, true}, + "ascGreater": {"10h10m", "2h5m", true, false}, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + less, ok := isDurationSort(u.asc, u.s1, u.s2) + assert.True(t, ok) + assert.Equal(t, u.e, less) + }) + } +} diff --git a/internal/views/sts.go b/internal/views/sts.go index dc8e18d5..4a2de1d7 100644 --- a/internal/views/sts.go +++ b/internal/views/sts.go @@ -16,6 +16,7 @@ type statefulSetView struct { func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer { v := statefulSetView{newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods return &v } @@ -23,7 +24,6 @@ func newStatefulSetView(t string, app *appView, list resource.List) resourceView func (v *statefulSetView) extraActions(aa keyActions) { aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(1, false), true) aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(2, false), true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *statefulSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -36,30 +36,25 @@ func (v *statefulSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey } } -func (v *statefulSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - ns, n := namespaced(v.selectedItem) - d := k8s.NewStatefulSet(v.app.conn()) - s, err := d.Get(ns, n) +func (v *statefulSetView) showPods(app *appView, ns, res, sel string) { + ns, n := namespaced(sel) + s := k8s.NewStatefulSet(app.conn()) + st, err := s.Get(ns, n) if err != nil { - log.Error().Err(err).Msgf("Fetching StatefulSet %s", v.selectedItem) - v.app.flash().errf("Unable to fetch statefulset %s", err) - return evt + log.Error().Err(err).Msgf("Fetching StatefulSet %s", sel) + app.flash().errf("Unable to fetch statefulset %s", err) + return } - sts := s.(*v1.StatefulSet) - sel, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector) + sts := st.(*v1.StatefulSet) + l, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector) if err != nil { - log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", v.selectedItem) - v.app.flash().errf("Selector failed %s", err) - return evt + log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", sel) + app.flash().errf("Selector failed %s", err) + return } - showPods(v.app, "", "StatefulSet", v.selectedItem, sel.String(), "", v.backCmd) - return nil + showPods(app, "", "StatefulSet", sel, l.String(), "", v.backCmd) } func (v *statefulSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/svc.go b/internal/views/svc.go index c2a7f2a0..a48734a3 100644 --- a/internal/views/svc.go +++ b/internal/views/svc.go @@ -22,6 +22,7 @@ type svcView struct { func newSvcView(t string, app *appView, list resource.List) resourceViewer { v := svcView{resourceView: newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions + v.enterFn = v.showPods return &v } @@ -29,7 +30,6 @@ func newSvcView(t string, app *appView, list resource.List) resourceViewer { func (v *svcView) extraActions(aa keyActions) { aa[tcell.KeyCtrlB] = newKeyAction("Bench", v.benchCmd, true) aa[KeyAltB] = newKeyAction("Bench Stop", v.benchStopCmd, true) - aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) aa[KeyShiftT] = newKeyAction("Sort Type", v.sortColCmd(1, false), true) } @@ -44,23 +44,19 @@ func (v *svcView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell } } -func (v *svcView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.rowSelected() { - return evt - } - - s := k8s.NewService(v.app.conn()) - ns, n := namespaced(v.selectedItem) - res, err := s.Get(ns, n) +func (v *svcView) showPods(app *appView, ns, res, sel string) { + s := k8s.NewService(app.conn()) + ns, n := namespaced(sel) + svc, err := s.Get(ns, n) if err != nil { - log.Error().Err(err).Msgf("Fetch service %s", v.selectedItem) - return nil - } - if svc, ok := res.(*v1.Service); ok { - v.showSvcPods(ns, svc.Spec.Selector, v.backCmd) + log.Error().Err(err).Msgf("Fetch service %s", sel) + app.flash().err(err) + return } - return nil + if s, ok := svc.(*v1.Service); ok { + v.showSvcPods(ns, s.Spec.Selector, v.backCmd) + } } func (v *svcView) backCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -111,15 +107,12 @@ func (v *svcView) benchCmd(evt *tcell.EventKey) *tcell.EventKey { } svcType := strings.TrimSpace(tv.GetCell(r, tv.nameColIndex()+1).Text) - log.Debug().Msgf("Service Type %q", svcType) if svcType != "NodePort" && svcType != "LoadBalancer" { v.app.flash().err(errors.New("You must select a reachable service")) return nil } ports := strings.TrimSpace(tv.GetCell(r, tv.nameColIndex()+5).Text) - // BOZO!! You Brute!! - // BOZO!! Will new much improv ie pop dialog and select port if multiport. pp := strings.Split(ports, " ") if len(pp) == 0 { v.app.flash().err(errors.New("No ports found")) diff --git a/internal/views/table.go b/internal/views/table.go index 4c1ceba6..6ee84da3 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -16,6 +16,7 @@ import ( "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/util/duration" ) const ( @@ -421,16 +422,23 @@ func (v *tableView) addHeaderCell(numCols map[string]bool, col int, name string, c := tview.NewTableCell(v.sortIndicator(col, name)) { c.SetExpansion(1) - c.SetTextColor(fg) if numCols[name] || cpuRX.MatchString(name) || memRX.MatchString(name) { c.SetAlign(tview.AlignRight) } + c.SetTextColor(fg) c.SetBackgroundColor(bg) } v.SetCell(0, col, c) } func (v *tableView) addBodyCell(numCols map[string]bool, header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) { + if header == "AGE" { + dur, err := time.ParseDuration(field) + if err == nil { + field = duration.HumanDuration(dur) + } + } + field += deltas(delta, field) align := tview.AlignLeft if numCols[header] || cpuRX.MatchString(header) || memRX.MatchString(header) { diff --git a/internal/views/table_test.go b/internal/views/table_test.go index e64bcdfd..e5dab9a7 100644 --- a/internal/views/table_test.go +++ b/internal/views/table_test.go @@ -47,6 +47,16 @@ func TestTVSortRows(t *testing.T) { resource.Row{"x", "y"}, []string{"row1", "row2"}, }, + { + resource.RowEvents{ + "row1": {Fields: resource.Row{"2175h48m0.06015s", "y"}}, + "row2": {Fields: resource.Row{"403h42m34.060166s", "b"}}, + }, + 0, + true, + resource.Row{"403h42m34.060166s", "b"}, + []string{"row2", "row1"}, + }, } var v *tableView