From 92ff64eac3814d0ba5614782a4c2e1162c4c8a19 Mon Sep 17 00:00:00 2001 From: Eric Bitencourt de Santana Date: Thu, 14 Nov 2024 12:56:52 -0300 Subject: [PATCH] Feature: add pod last restart column to pod section (#2946) * feat: create ToRestartAge helper function to calculate last restart age * feat: add Last Restart column to pod view * test: add TestPodLastRestart to pod tests * test: add TestToRestartAge tests * test: fix all initial row states on table tests * chore: remove TestToRestartAge changes * refactor: move LastRestart pod function to internal function * refactor: update to new go 1.22 range over map syntax --- internal/model/table_int_test.go | 2 +- internal/model/table_test.go | 2 +- internal/render/helpers_test.go | 2 +- internal/render/pod.go | 18 +++++++ internal/render/pod_int_test.go | 86 ++++++++++++++++++++++++++++++++ internal/render/pod_test.go | 12 ++--- 6 files changed, 113 insertions(+), 9 deletions(-) diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index ea7c0390..e10bf36a 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -35,7 +35,7 @@ func TestTableReconcile(t *testing.T) { err := ta.reconcile(ctx) assert.Nil(t, err) data := ta.Peek() - assert.Equal(t, 23, data.HeaderCount()) + assert.Equal(t, 24, data.HeaderCount()) assert.Equal(t, 1, data.RowCount()) assert.Equal(t, client.NamespaceAll, data.GetNamespace()) } diff --git a/internal/model/table_test.go b/internal/model/table_test.go index 54171017..05dd64e3 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -36,7 +36,7 @@ func TestTableRefresh(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) assert.NoError(t, ta.Refresh(ctx)) data := ta.Peek() - assert.Equal(t, 23, data.HeaderCount()) + assert.Equal(t, 24, data.HeaderCount()) assert.Equal(t, 1, data.RowCount()) assert.Equal(t, client.NamespaceAll, data.GetNamespace()) assert.Equal(t, 1, l.count) diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 0c6d0787..df4747e6 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -54,7 +54,7 @@ func TestTableHydrate(t *testing.T) { assert.Nil(t, model1.Hydrate("blee", oo, rr, Pod{})) assert.Equal(t, 1, len(rr)) - assert.Equal(t, 23, len(rr[0].Fields)) + assert.Equal(t, 24, len(rr[0].Fields)) } func TestToAge(t *testing.T) { diff --git a/internal/render/pod.go b/internal/render/pod.go index 7366257d..bb9ce9bb 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -12,6 +12,7 @@ import ( "github.com/derailed/tview" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -92,6 +93,7 @@ func (p Pod) Header(ns string) model1.Header { model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LAST RESTART", Align: tview.AlignRight, Time: true, Wide: true}, model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, @@ -127,6 +129,7 @@ func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { _, _, irc := p.Statuses(ics) cs := po.Status.ContainerStatuses cr, _, rc := p.Statuses(cs) + lr := p.lastRestart(cs) var ccmx []mv1beta1.ContainerMetrics if pwm.MX != nil { @@ -144,6 +147,7 @@ func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { strconv.Itoa(cr) + "/" + strconv.Itoa(len(po.Spec.Containers)), phase, strconv.Itoa(rc + irc), + ToAge(lr), toMc(c.cpu), toMi(c.mem), toMc(r.cpu) + ":" + toMc(r.lcpu), @@ -317,6 +321,20 @@ func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { return } +// lastRestart returns the last container restart time. +func (*Pod) lastRestart(ss []v1.ContainerStatus) (latest metav1.Time) { + for _, c := range ss { + if c.LastTerminationState.Terminated == nil { + continue + } + ts := c.LastTerminationState.Terminated.FinishedAt + if latest.IsZero() || ts.After(latest.Time) { + latest = ts + } + } + return +} + // Phase reports the given pod phase. func (p *Pod) Phase(po *v1.Pod) string { status := string(po.Status.Phase) diff --git a/internal/render/pod_int_test.go b/internal/render/pod_int_test.go index c29f230d..d135aa5d 100644 --- a/internal/render/pod_int_test.go +++ b/internal/render/pod_int_test.go @@ -4,12 +4,15 @@ package render import ( + "fmt" "testing" + "time" "github.com/derailed/k9s/internal/client" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" res "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -354,6 +357,81 @@ func Test_filterSidecarCO(t *testing.T) { } } +func Test_lastRestart(t *testing.T) { + uu := map[string]struct { + containerStatuses []v1.ContainerStatus + expected metav1.Time + }{ + "no-restarts": { + containerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + LastTerminationState: v1.ContainerState{}, + }, + }, + expected: metav1.Time{}, + }, + "single-container-restart": { + containerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + FinishedAt: metav1.Time{Time: testTime()}, + }, + }, + }, + }, + expected: metav1.Time{Time: testTime()}, + }, + "multiple-container-restarts": { + containerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + FinishedAt: metav1.Time{Time: testTime().Add(-1 * time.Hour)}, + }, + }, + }, + { + Name: "c2", + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + FinishedAt: metav1.Time{Time: testTime()}, + }, + }, + }, + }, + expected: metav1.Time{Time: testTime()}, + }, + "mixed-termination-states": { + containerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + LastTerminationState: v1.ContainerState{}, + }, + { + Name: "c2", + LastTerminationState: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + FinishedAt: metav1.Time{Time: testTime()}, + }, + }, + }, + }, + expected: metav1.Time{Time: testTime()}, + }, + } + + var p Pod + for name, u := range uu { + t.Run(name, func(t *testing.T) { + assert.Equal(t, u.expected, p.lastRestart(u.containerStatuses)) + }) + } +} + func Test_gatherPodMx(t *testing.T) { uu := map[string]struct { spec *v1.PodSpec @@ -546,3 +624,11 @@ func makeCoMX(n string, c, m string) mv1beta1.ContainerMetrics { Usage: makeRes(c, m), } } + +func testTime() time.Time { + t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") + if err != nil { + fmt.Println("TestTime Failed", err) + } + return t +} diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go index 92b35ac0..e8534b52 100644 --- a/internal/render/pod_test.go +++ b/internal/render/pod_test.go @@ -163,8 +163,8 @@ func TestPodRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""} - assert.Equal(t, e, r.Fields[:19]) + e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""} + assert.Equal(t, e, r.Fields[:20]) } func BenchmarkPodRender(b *testing.B) { @@ -194,8 +194,8 @@ func TestPodInitRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""} - assert.Equal(t, e, r.Fields[:19]) + e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""} + assert.Equal(t, e, r.Fields[:20]) } func TestPodSidecarRender(t *testing.T) { @@ -210,8 +210,8 @@ func TestPodSidecarRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/sleep", r.ID) - e := model1.Fields{"default", "sleep", "0", "●", "1/1", "Running", "0", "100", "40", "50:250", "50:80", "200", "40", "80", "50", "10.244.0.8", "kind-control-plane", "", ""} - assert.Equal(t, e, r.Fields[:19]) + e := model1.Fields{"default", "sleep", "0", "●", "1/1", "Running", "0", "", "100", "40", "50:250", "50:80", "200", "40", "80", "50", "10.244.0.8", "kind-control-plane", "", ""} + assert.Equal(t, e, r.Fields[:20]) } func TestCheckPodStatus(t *testing.T) {