k9s/internal/render/pod_test.go

670 lines
14 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render_test
import (
"testing"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tcell/v2"
"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"
)
func init() {
render.AddColor = tcell.ColorBlue
render.HighlightColor = tcell.ColorYellow
render.CompletedColor = tcell.ColorGray
render.StdColor = tcell.ColorWhite
render.ErrColor = tcell.ColorRed
render.KillColor = tcell.ColorGray
}
func TestPodColorer(t *testing.T) {
stdHeader := render.Header{
render.HeaderColumn{Name: "NAMESPACE"},
render.HeaderColumn{Name: "NAME"},
render.HeaderColumn{Name: "READY"},
render.HeaderColumn{Name: "RESTARTS"},
render.HeaderColumn{Name: "STATUS"},
render.HeaderColumn{Name: "VALID"},
}
uu := map[string]struct {
re render.RowEvent
h render.Header
e tcell.Color
}{
"valid": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Running, ""},
},
},
e: render.StdColor,
},
"init": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""},
},
},
e: render.AddColor,
},
"init-err": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"},
},
},
e: render.AddColor,
},
"initialized": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"},
},
},
e: render.HighlightColor,
},
"completed": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"},
},
},
e: render.CompletedColor,
},
"terminating": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"},
},
},
e: render.KillColor,
},
"invalid": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "Running", "blah"},
},
},
e: render.ErrColor,
},
"unknown-cool": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""},
},
},
e: render.AddColor,
},
"unknown-err": {
h: stdHeader,
re: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", "doh"},
},
},
e: render.ErrColor,
},
"status": {
h: stdHeader[0:3],
re: render.RowEvent{
Kind: render.EventDelete,
Row: render.Row{
Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""},
},
},
e: render.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"),
}
var po render.Pod
r := render.NewRow(14)
err := po.Render(&pom, "", &r)
assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID)
e := render.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])
}
func BenchmarkPodRender(b *testing.B) {
pom := render.PodWithMetrics{
Raw: load(b, "po"),
MX: makePodMX("nginx", "10m", "10Mi"),
}
var po render.Pod
r := render.NewRow(12)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = po.Render(&pom, "", &r)
}
}
func TestPodInitRender(t *testing.T) {
pom := render.PodWithMetrics{
Raw: load(t, "po_init"),
MX: makePodMX("nginx", "10m", "10Mi"),
}
var po render.Pod
r := render.NewRow(14)
err := po.Render(&pom, "", &r)
assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID)
e := render.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])
}
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))
})
}
}
// ----------------------------------------------------------------------------
// 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,
}
}