k9s/internal/render/pod_int_test.go

818 lines
16 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import (
"fmt"
"testing"
"time"
"github.com/derailed/k9s/internal/client"
"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 Test_checkInitContainerStatus(t *testing.T) {
trueVal := true
uu := map[string]struct {
status v1.ContainerStatus
e string
count, total int
restart bool
}{
"none": {
e: "Init:0/0",
},
"restart": {
status: v1.ContainerStatus{
Name: "ic1",
Started: &trueVal,
State: v1.ContainerState{},
},
restart: true,
e: "Init:0/0",
},
"no-restart": {
status: v1.ContainerStatus{
Name: "ic1",
Started: &trueVal,
State: v1.ContainerState{},
},
e: "Init:0/0",
},
"terminated-reason": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 1,
Reason: "blah",
},
},
},
e: "Init:blah",
},
"terminated-signal": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 1,
Signal: 9,
},
},
},
e: "Init:Signal:9",
},
"terminated-code": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 1,
},
},
},
e: "Init:ExitCode:1",
},
"terminated-restart": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
Reason: "blah",
},
},
},
},
"waiting": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
Reason: "blah",
},
},
},
e: "Init:blah",
},
"waiting-init": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
Reason: "PodInitializing",
},
},
},
e: "Init:0/0",
},
"running": {
status: v1.ContainerStatus{
Name: "ic1",
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
e: "Init:0/0",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, checkInitContainerStatus(&u.status, u.count, u.total, u.restart))
})
}
}
func Test_containerPhase(t *testing.T) {
uu := map[string]struct {
status v1.PodStatus
e string
ok bool
}{
"none": {},
"empty": {
status: v1.PodStatus{
Phase: PhaseUnknown,
},
},
"waiting": {
status: v1.PodStatus{
Phase: PhaseUnknown,
InitContainerStatuses: []v1.ContainerStatus{
{
Name: "ic1",
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
Reason: "waiting",
},
},
},
},
},
e: "waiting",
},
"terminated": {
status: v1.PodStatus{
Phase: PhaseUnknown,
InitContainerStatuses: []v1.ContainerStatus{
{
Name: "ic1",
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
Reason: "done",
},
},
},
},
},
e: "done",
},
"terminated-sig": {
status: v1.PodStatus{
Phase: PhaseUnknown,
InitContainerStatuses: []v1.ContainerStatus{
{
Name: "ic1",
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
Signal: 9,
},
},
},
},
},
e: "Signal:9",
},
"terminated-code": {
status: v1.PodStatus{
Phase: PhaseUnknown,
InitContainerStatuses: []v1.ContainerStatus{
{
Name: "ic1",
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 2,
},
},
},
},
},
e: "ExitCode:2",
},
"running": {
status: v1.PodStatus{
Phase: PhaseUnknown,
InitContainerStatuses: []v1.ContainerStatus{
{
Name: "ic1",
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
},
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
Ready: true,
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{},
},
},
},
},
ok: true,
},
}
var p Pod
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
s, ok := p.containerPhase(&u.status, "")
assert.Equal(t, u.ok, ok)
assert.Equal(t, u.e, s)
})
}
}
func Test_isSideCarContainer(t *testing.T) {
always, never := v1.ContainerRestartPolicyAlways, v1.ContainerRestartPolicy("never")
uu := map[string]struct {
p *v1.ContainerRestartPolicy
e bool
}{
"empty": {},
"sidecar": {
p: &always,
e: true,
},
"no-sidecar": {
p: &never,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, isSideCarContainer(u.p))
})
}
}
func Test_filterSidecarCO(t *testing.T) {
always := v1.ContainerRestartPolicyAlways
uu := map[string]struct {
cc, ecc []v1.Container
}{
"empty": {
cc: []v1.Container{},
ecc: []v1.Container{},
},
"restartable": {
cc: []v1.Container{
{
Name: "c1",
RestartPolicy: &always,
},
},
ecc: []v1.Container{
{
Name: "c1",
RestartPolicy: &always,
},
},
},
"not-restartable": {
cc: []v1.Container{
{
Name: "c1",
},
},
ecc: []v1.Container{},
},
"mixed": {
cc: []v1.Container{
{
Name: "c1",
},
{
Name: "c2",
RestartPolicy: &always,
},
},
ecc: []v1.Container{
{
Name: "c2",
RestartPolicy: &always,
},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.ecc, filterSidecarCO(u.cc))
})
}
}
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) {
_, _, _, lr := p.ContainerStats(u.containerStatuses)
assert.Equal(t, u.expected, lr)
})
}
}
func Test_gatherPodMX(t *testing.T) {
uu := map[string]struct {
spec *v1.PodSpec
mx []mv1beta1.ContainerMetrics
c, r metric
perc string
}{
"single": {
spec: &v1.PodSpec{
Containers: []v1.Container{
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
},
},
mx: []mv1beta1.ContainerMetrics{
makeCoMX("c1", "1m", "22Mi"),
},
c: metric{
cpu: 1,
mem: 22 * client.MegaByte,
gpu: 1,
},
r: metric{
cpu: 10,
mem: 1 * client.MegaByte,
gpu: 1,
lcpu: 20,
lmem: 2 * client.MegaByte,
lgpu: 1,
},
perc: "10",
},
"multi": {
spec: &v1.PodSpec{
Containers: []v1.Container{
makeContainer("c1", false, "11m", "22Mi", "111m", "44Mi"),
makeContainer("c2", false, "93m", "1402Mi", "0m", "2804Mi"),
makeContainer("c3", false, "11m", "34Mi", "0m", "69Mi"),
},
},
r: metric{
cpu: 11 + 93 + 11,
gpu: 1,
mem: (22 + 1402 + 34) * client.MegaByte,
lcpu: 111 + 0 + 0,
lgpu: 1,
lmem: (44 + 2804 + 69) * client.MegaByte,
},
mx: []mv1beta1.ContainerMetrics{
makeCoMX("c1", "1m", "22Mi"),
makeCoMX("c2", "51m", "1275Mi"),
makeCoMX("c3", "1m", "27Mi"),
},
c: metric{
cpu: 1 + 51 + 1,
gpu: 1,
mem: (22 + 1275 + 27) * client.MegaByte,
},
perc: "46",
},
"sidecar": {
spec: &v1.PodSpec{
Containers: []v1.Container{
makeContainer("c1", false, "11m", "22Mi", "111m", "44Mi"),
},
InitContainers: []v1.Container{
makeContainer("c2", true, "93m", "1402Mi", "0m", "2804Mi"),
},
},
r: metric{
cpu: 11 + 93,
gpu: 1,
mem: (22 + 1402) * client.MegaByte,
lcpu: 111 + 0,
lgpu: 1,
lmem: (44 + 2804) * client.MegaByte,
},
mx: []mv1beta1.ContainerMetrics{
makeCoMX("c1", "1m", "22Mi"),
makeCoMX("c2", "51m", "1275Mi"),
},
c: metric{
cpu: 1 + 51,
gpu: 1,
mem: (22 + 1275) * client.MegaByte,
},
perc: "50",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c, r := gatherPodMX(u.spec, u.mx)
assert.Equal(t, u.c.cpu, c.cpu)
assert.Equal(t, u.c.mem, c.mem)
assert.Equal(t, u.c.lcpu, c.lcpu)
assert.Equal(t, u.c.lmem, c.lmem)
assert.Equal(t, u.c.lgpu, c.lgpu)
assert.Equal(t, u.r.cpu, r.cpu)
assert.Equal(t, u.r.mem, r.mem)
assert.Equal(t, u.r.lcpu, r.lcpu)
assert.Equal(t, u.r.lmem, r.lmem)
assert.Equal(t, u.r.gpu, r.gpu)
assert.Equal(t, u.r.lgpu, r.lgpu)
assert.Equal(t, u.perc, client.ToPercentageStr(c.cpu, r.cpu))
})
}
}
func Test_podLimits(t *testing.T) {
uu := map[string]struct {
cc []v1.Container
l v1.ResourceList
}{
"plain": {
cc: []v1.Container{
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
},
l: makeRes("20m", "2Mi"),
},
"multi-co": {
cc: []v1.Container{
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
makeContainer("c2", false, "10m", "1Mi", "40m", "4Mi"),
},
l: makeRes("60m", "6Mi"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c, m, g := cosLimits(u.cc)
assert.True(t, c.Equal(*u.l.Cpu()))
assert.True(t, m.Equal(*u.l.Memory()))
assert.True(t, g.Equal(*extractGPU(u.l)))
})
}
}
func Test_podRequests(t *testing.T) {
uu := map[string]struct {
cc []v1.Container
e v1.ResourceList
}{
"plain": {
cc: []v1.Container{
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
},
e: makeRes("10m", "1Mi"),
},
"multi-co": {
cc: []v1.Container{
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
makeContainer("c2", false, "10m", "1Mi", "40m", "4Mi"),
},
e: makeRes("20m", "2Mi"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c, m, g := cosRequests(u.cc)
assert.True(t, c.Equal(*u.e.Cpu()))
assert.True(t, m.Equal(*u.e.Memory()))
assert.True(t, g.Equal(*extractGPU(u.e)))
})
}
}
func Test_readinessGateStats(t *testing.T) {
const (
gate1 = "k9s.derailed.com/gate1"
gate2 = "k9s.derailed.com/gate2"
)
uu := map[string]struct {
spec *v1.PodSpec
st *v1.PodStatus
r int
t int
}{
"empty": {
spec: &v1.PodSpec{},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue}},
},
r: 0,
t: 0,
},
"single": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}},
},
r: 1,
t: 1,
},
"multiple": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: gate2, Status: v1.ConditionTrue}, {Type: v1.PodReady, Status: v1.ConditionFalse}},
},
r: 2,
t: 2,
},
"mixed": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: gate2, Status: v1.ConditionFalse}, {Type: v1.PodReady, Status: v1.ConditionTrue}},
},
r: 1,
t: 2,
},
"missing": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: v1.PodReady, Status: v1.ConditionTrue}},
},
r: 1,
t: 2,
},
}
var p Pod
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
ready, total := p.readinessGateStats(u.spec, u.st)
assert.Equal(t, u.r, ready)
assert.Equal(t, u.t, total)
})
}
}
func Test_diagnose(t *testing.T) {
uu := map[string]struct {
phase string
cr, ct int
ready bool
rgr, rgt int
err string
}{
"completed": {
phase: Completed,
cr: 0,
ct: 1,
ready: true,
rgr: 0,
rgt: 0,
err: "",
},
"container-ready-check-failed": {
phase: "Running",
cr: 1,
ct: 2,
ready: true,
rgr: 1,
rgt: 2,
err: "container ready check failed: 1 of 2",
},
"readiness-gate-check-failed": {
phase: "Running",
cr: 1,
ct: 1,
ready: true,
rgr: 1,
rgt: 2,
err: "readiness gate check failed: 1 of 2",
},
"pod-condition-ready-false": {
phase: "Running",
cr: 1,
ct: 1,
ready: false,
rgr: 0,
rgt: 0,
err: "pod condition ready is false",
},
"pod-terminating": {
phase: "Terminating",
cr: 1,
ct: 1,
ready: true,
rgr: 1,
rgt: 1,
err: "pod is terminating",
},
}
var p Pod
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
err := p.diagnose(u.phase, u.cr, u.ct, u.ready, u.rgr, u.rgt)
if u.err == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), u.err)
}
})
}
}
// Helpers...
func makeContainer(n string, restartable bool, rc, rm, lc, lm string) v1.Container {
always := v1.ContainerRestartPolicyAlways
rq := v1.ResourceRequirements{
Requests: makeRes(rc, rm),
Limits: makeRes(lc, lm),
}
var rp *v1.ContainerRestartPolicy
if restartable {
rp = &always
}
return v1.Container{Name: n, Resources: rq, RestartPolicy: rp}
}
func makeRes(c, m string) v1.ResourceList {
cpu, _ := res.ParseQuantity(c)
mem, _ := res.ParseQuantity(m)
gpu, _ := res.ParseQuantity(c)
return v1.ResourceList{
v1.ResourceCPU: cpu,
v1.ResourceMemory: mem,
v1.ResourceName("nvidia.com/gpu"): gpu,
}
}
func makeCoMX(n, c, m string) mv1beta1.ContainerMetrics {
return mv1beta1.ContainerMetrics{
Name: n,
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
}