// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package render_test import ( "testing" "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" 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" ) func init() { model1.AddColor = tcell.ColorBlue model1.HighlightColor = tcell.ColorYellow model1.CompletedColor = tcell.ColorGray model1.StdColor = tcell.ColorWhite model1.ErrColor = tcell.ColorRed model1.KillColor = tcell.ColorGray } func TestPodColorer(t *testing.T) { stdHeader := model1.Header{ model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "RESTARTS"}, model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "VALID"}, } uu := map[string]struct { re model1.RowEvent h model1.Header e tcell.Color }{ "valid": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Running, ""}, }, }, e: model1.StdColor, }, "init": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""}, }, }, e: model1.AddColor, }, "init-err": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"}, }, }, e: model1.AddColor, }, "initialized": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"}, }, }, e: model1.HighlightColor, }, "completed": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"}, }, }, e: model1.CompletedColor, }, "terminating": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"}, }, }, e: model1.KillColor, }, "invalid": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", "Running", "blah"}, }, }, e: model1.ErrColor, }, "unknown-cool": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""}, }, }, e: model1.AddColor, }, "unknown-err": { h: stdHeader, re: model1.RowEvent{ Kind: model1.EventAdd, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", "doh"}, }, }, e: model1.ErrColor, }, "status": { h: stdHeader[0:3], re: model1.RowEvent{ Kind: model1.EventDelete, Row: model1.Row{ Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""}, }, }, e: model1.KillColor, }, } var r render.Pod for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, r.ColorerFunc()("", u.h, &u.re)) }) } } func TestPodRender(t *testing.T) { pom := render.PodWithMetrics{ Raw: load(t, "po"), MX: makePodMX("nginx", "100m", "50Mi"), } po := render.NewPod() r := model1.NewRow(14) err := po.Render(&pom, "", &r) require.NoError(t, err) assert.Equal(t, "default/nginx", r.ID) e := model1.Fields{"default", "nginx", "n/a", "●", "1/1", "Running", "0", "", "100", "100:0", "100", "n/a", "50", "70:170", "71", "29", "0:0", "172.17.0.6", "minikube", "default", ""} assert.Equal(t, e, r.Fields[:21]) } func BenchmarkPodRender(b *testing.B) { pom := render.PodWithMetrics{ Raw: load(b, "po"), MX: makePodMX("nginx", "10m", "10Mi"), } po := render.NewPod() r := model1.NewRow(12) b.ReportAllocs() b.ResetTimer() for range b.N { _ = po.Render(&pom, "", &r) } } func TestPodInitRender(t *testing.T) { pom := render.PodWithMetrics{ Raw: load(t, "po_init"), MX: makePodMX("nginx", "10m", "10Mi"), } po := render.NewPod() r := model1.NewRow(14) err := po.Render(&pom, "", &r) require.NoError(t, err) assert.Equal(t, "default/nginx", r.ID) e := model1.Fields{"default", "nginx", "n/a", "●", "1/1", "Init:0/1", "0", "", "10", "100:0", "10", "n/a", "10", "70:170", "14", "5", "0:0", "172.17.0.6", "minikube", "default", ""} assert.Equal(t, e, r.Fields[:21]) } func TestPodSidecarRender(t *testing.T) { pom := render.PodWithMetrics{ Raw: load(t, "po_sidecar"), MX: makePodMX("sleep", "100m", "40Mi"), } po := render.NewPod() r := model1.NewRow(14) err := po.Render(&pom, "", &r) require.NoError(t, err) assert.Equal(t, "default/sleep", r.ID) e := model1.Fields{"default", "sleep", "n/a", "●", "2/2", "Running", "0", "", "100", "50:250", "200", "40", "40", "50:80", "80", "50", "0:0", "10.244.0.8", "kind-control-plane", "default", ""} assert.Equal(t, e, r.Fields[:21]) } func TestCheckPodStatus(t *testing.T) { uu := map[string]struct { pod v1.Pod e string }{ "unknown": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: render.PhaseUnknown, }, }, e: render.PhaseUnknown, }, "running": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{}, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Running: &v1.ContainerStateRunning{}, }, }, }, }, }, e: render.PhaseRunning, }, "gated": { pod: v1.Pod{ Status: v1.PodStatus{ Conditions: []v1.PodCondition{ {Type: v1.PodScheduled, Reason: v1.PodReasonSchedulingGated}, }, Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{}, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Running: &v1.ContainerStateRunning{}, }, }, }, }, }, e: v1.PodReasonSchedulingGated, }, "backoff": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, }, }, e: render.PhaseImagePullBackOff, }, "backoff-init": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, }, }, e: "Init:ImagePullBackOff", }, "init-terminated-cool": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", State: v1.ContainerState{}, }, }, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, }, }, e: "Init:0/0", }, "init-terminated-reason": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 1, Reason: "blah", }, }, }, }, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, }, }, e: "Init:blah", }, "init-terminated-sig": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 2, Signal: 9, }, }, }, }, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, }, }, e: "Init:Signal:9", }, "init-terminated-code": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 2, }, }, }, }, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: render.PhaseImagePullBackOff, }, }, }, }, }, }, e: "Init:ExitCode:2", }, "co-reason": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ Reason: "blah", }, }, }, }, }, }, e: "blah", }, "co-reason-ready": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", Ready: true, State: v1.ContainerState{ Running: &v1.ContainerStateRunning{}, }, }, }, }, }, e: "Running", }, "co-reason-completed": { pod: v1.Pod{ Status: v1.PodStatus{ Conditions: []v1.PodCondition{ {Type: v1.PodReady, Status: v1.ConditionTrue}, }, Phase: render.PhaseCompleted, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", Ready: true, State: v1.ContainerState{ Running: &v1.ContainerStateRunning{}, }, }, }, }, }, e: "Running", }, "co-sig": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 2, Signal: 9, }, }, }, }, }, }, e: "Signal:9", }, "co-code": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ ExitCode: 2, }, }, }, }, }, }, e: "ExitCode:2", }, "co-ready": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Running: &v1.ContainerStateRunning{}, }, }, }, }, }, e: "Running", }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, render.PodStatus(&u.pod)) }) } } func TestCheckPhase(t *testing.T) { always := v1.ContainerRestartPolicyAlways uu := map[string]struct { pod v1.Pod e string }{ "unknown": { pod: v1.Pod{ Status: v1.PodStatus{ Phase: render.PhaseUnknown, }, }, e: render.PhaseUnknown, }, "terminating": { pod: v1.Pod{ ObjectMeta: metav1.ObjectMeta{ DeletionTimestamp: &metav1.Time{Time: testTime()}, }, Status: v1.PodStatus{ Phase: render.PhaseUnknown, Reason: "bla", }, }, e: render.PhaseTerminating, }, "terminating-toast-node": { pod: v1.Pod{ ObjectMeta: metav1.ObjectMeta{ DeletionTimestamp: &metav1.Time{Time: testTime()}, }, Status: v1.PodStatus{ Phase: render.PhaseUnknown, Reason: render.NodeUnreachablePodReason, }, }, e: render.PhaseUnknown, }, "restartable": { pod: v1.Pod{ ObjectMeta: metav1.ObjectMeta{ DeletionTimestamp: &metav1.Time{Time: testTime()}, }, Spec: v1.PodSpec{ InitContainers: []v1.Container{ { Name: "ic1", RestartPolicy: &always, }, }, }, Status: v1.PodStatus{ Phase: render.PhaseUnknown, Reason: "bla", InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", }, }, }, }, e: "Init:0/1", }, "waiting": { pod: v1.Pod{ ObjectMeta: metav1.ObjectMeta{ DeletionTimestamp: &metav1.Time{Time: testTime()}, }, Spec: v1.PodSpec{ InitContainers: []v1.Container{ { Name: "ic1", RestartPolicy: &always, }, }, Containers: []v1.Container{ { Name: "c1", }, }, }, Status: v1.PodStatus{ Phase: render.PhaseUnknown, Reason: "bla", InitContainerStatuses: []v1.ContainerStatus{ { Name: "ic1", State: v1.ContainerState{ Running: &v1.ContainerStateRunning{}, }, }, }, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ Reason: "bla", }, }, }, }, }, }, e: "Init:0/1", }, } var p render.Pod for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, p.Phase(u.pod.DeletionTimestamp, &u.pod.Spec, &u.pod.Status)) }) } } // ---------------------------------------------------------------------------- // Helpers... func makePodMX(name, cpu, mem string) *mv1beta1.PodMetrics { return &mv1beta1.PodMetrics{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "default", }, Containers: []mv1beta1.ContainerMetrics{ {Usage: makeRes(cpu, mem)}, }, } } func makeRes(c, m string) v1.ResourceList { cpu, _ := res.ParseQuantity(c) mem, _ := res.ParseQuantity(m) return v1.ResourceList{ v1.ResourceCPU: cpu, v1.ResourceMemory: mem, } }