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
mine
Eric Bitencourt de Santana 2024-11-14 12:56:52 -03:00 committed by GitHub
parent 9984e3f4bf
commit 92ff64eac3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 113 additions and 9 deletions

View File

@ -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())
}

View File

@ -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)

View File

@ -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) {

View File

@ -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)

View File

@ -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
}

View File

@ -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", "<none>", "<none>"}
assert.Equal(t, e, r.Fields[:19])
e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "<unknown>", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "<none>", "<none>"}
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", "<none>", "<none>"}
assert.Equal(t, e, r.Fields[:19])
e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "<unknown>", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "<none>", "<none>"}
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", "<none>", "<none>"}
assert.Equal(t, e, r.Fields[:19])
e := model1.Fields{"default", "sleep", "0", "●", "1/1", "Running", "0", "<unknown>", "100", "40", "50:250", "50:80", "200", "40", "80", "50", "10.244.0.8", "kind-control-plane", "<none>", "<none>"}
assert.Equal(t, e, r.Fields[:20])
}
func TestCheckPodStatus(t *testing.T) {