internal/checks/lifecycle_test.go
Rama main · réplica de lectura del repositorio original.
package checks
import (
"maps"
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func httpProbe(port any, delay int64) map[string]any {
return map[string]any{
"httpGet": map[string]any{"path": "/healthz", "port": port},
"initialDelaySeconds": delay,
}
}
func settledContainer(fields map[string]any) map[string]any {
base := map[string]any{
"ports": []any{map[string]any{"name": "http", "containerPort": int64(8080)}},
"livenessProbe": httpProbe(int64(8080), int64(10)),
"readinessProbe": map[string]any{"tcpSocket": map[string]any{"port": int64(8080)}},
"lifecycle": map[string]any{"preStop": map[string]any{"exec": map[string]any{"command": []any{"sleep", "5"}}}},
"resources": map[string]any{
"requests": map[string]any{"memory": "256Mi", ephemeralName: "1Gi"},
"limits": map[string]any{"memory": "256Mi", ephemeralName: "1Gi"},
},
}
maps.Copy(base, fields)
return container("app", base)
}
func settledPod(fields map[string]any, containers ...map[string]any) map[string]any {
base := map[string]any{
graceField: int64(30),
"topologySpreadConstraints": []any{map[string]any{"maxSkew": int64(1)}},
}
maps.Copy(base, fields)
return podSpecWith(base, containers...)
}
func settledDeployment(pod map[string]any) *unstructured.Unstructured {
obj := replicas(deployment("api", pod), 2)
spec := specOf(obj)
spec["strategy"] = map[string]any{
"type": "RollingUpdate",
"rollingUpdate": map[string]any{"maxUnavailable": int64(1)},
}
spec[revisionHistory] = int64(3)
return obj
}
func settled() *unstructured.Unstructured {
return settledDeployment(settledPod(nil, settledContainer(nil)))
}
func withSpec(obj *unstructured.Unstructured, key string, value any) *unstructured.Unstructured {
specOf(obj)[key] = value
return obj
}
func TestEveryLifecycleCheckFiresOnItsOwnFaultAndOnNothingElse(t *testing.T) {
cases := []struct {
id string
trips *unstructured.Unstructured
}{
{
id: "probes-identical",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"readinessProbe": httpProbe(int64(8080), int64(10)),
}))),
},
{
id: "probe-port-not-declared",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": httpProbe(int64(9090), int64(10)),
}))),
},
{
id: "liveness-without-startup-grace",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": httpProbe(int64(8080), int64(0)),
}))),
},
{
id: "no-prestop-hook",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"lifecycle": nil,
}))),
},
{
id: "grace-period-zero",
trips: settledDeployment(settledPod(map[string]any{graceField: int64(0)}, settledContainer(nil))),
},
{
id: "grace-period-blocks-drain",
trips: settledDeployment(settledPod(map[string]any{graceField: int64(1800)}, settledContainer(nil))),
},
{
id: "wrong-restart-policy",
trips: settledDeployment(settledPod(map[string]any{"restartPolicy": "OnFailure"}, settledContainer(nil))),
},
{
id: "ephemeral-storage-unset",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"resources": map[string]any{
"requests": map[string]any{"memory": "256Mi"},
"limits": map[string]any{"memory": "256Mi"},
},
}))),
},
{
id: "memory-limit-not-request",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"resources": map[string]any{
"requests": map[string]any{"memory": "128Mi", ephemeralName: "1Gi"},
"limits": map[string]any{"memory": "256Mi", ephemeralName: "1Gi"},
},
}))),
},
{
id: "init-container-unbounded",
trips: settledDeployment(settledPod(map[string]any{
"initContainers": []any{map[string]any{"name": "setup", "image": "busybox:1.36"}},
}, settledContainer(nil))),
},
{
id: "replicas-zero",
trips: withSpec(settled(), "replicas", int64(0)),
},
{
id: "no-spread-no-anti-affinity",
trips: settledDeployment(settledPod(map[string]any{
"topologySpreadConstraints": nil,
}, settledContainer(nil))),
},
{
id: "recreate-strategy",
trips: withSpec(settled(), "strategy", map[string]any{"type": recreateShape}),
},
{
id: "max-unavailable-all",
trips: withSpec(settled(), "strategy", map[string]any{
"type": "RollingUpdate",
"rollingUpdate": map[string]any{"maxUnavailable": "100%"},
}),
},
{
id: "statefulset-no-service-name",
trips: workload("StatefulSet", "db", settledPod(nil, settledContainer(nil))),
},
{
id: "unbounded-revision-history",
trips: withSpec(settled(), revisionHistory, int64(50)),
},
{
id: "duplicate-env-keys",
trips: settledDeployment(settledPod(nil, settledContainer(map[string]any{
"env": []any{
map[string]any{"name": "MODE", "value": "a"},
map[string]any{"name": "MODE", "value": "b"},
},
}))),
},
}
registered := map[string]bool{}
for _, entry := range lifecycleChecks() {
registered[entry.id] = true
}
if len(cases) != len(registered) {
t.Fatalf("%d cases cover %d registered lifecycle checks", len(cases), len(registered))
}
for _, tc := range cases {
t.Run(tc.id, func(t *testing.T) {
if !registered[tc.id] {
t.Fatalf("%s is not a registered lifecycle check", tc.id)
}
if findingCount(t, report(t, tc.trips), tc.id) == 0 {
t.Fatalf("%s did not fire on the workload written to trip it", tc.id)
}
if findingCount(t, report(t, settled()), tc.id) != 0 {
t.Fatalf("%s fired on a settled deployment", tc.id)
}
})
}
}
func TestProbesThatShareAHandlerKindButNotItsFieldsAreNotIdentical(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"readinessProbe": map[string]any{
"httpGet": map[string]any{"path": "/ready", "port": int64(8080)},
},
}))))
if findingCount(t, found, "probes-identical") != 0 {
t.Fatal("two http probes on different paths were called identical")
}
}
func TestProbesOnDifferentHandlerKindsAreNotIdentical(t *testing.T) {
found := report(t, settled())
if findingCount(t, found, "probes-identical") != 0 {
t.Fatal("an http liveness and a tcp readiness were called identical")
}
}
func TestAProbeNamedPortIsMatchedAgainstTheDeclaredNames(t *testing.T) {
named := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": httpProbe("http", int64(10)),
}))))
if findingCount(t, named, "probe-port-not-declared") != 0 {
t.Fatal("a probe on the declared port name was reported as undeclared")
}
wrong := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": httpProbe("metrics", int64(10)),
}))))
if !strings.Contains(onlyFinding(t, wrong, "probe-port-not-declared").Detail, "metrics") {
t.Fatal("a probe on an undeclared port name was not reported")
}
}
func TestAContainerThatDeclaresNoPortsIsNotProbeChecked(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"ports": nil,
"livenessProbe": httpProbe(int64(9090), int64(10)),
}))))
if findingCount(t, found, "probe-port-not-declared") != 0 {
t.Fatal("a container that declares no ports at all was reported")
}
}
func TestAStartupProbeSatisfiesTheLivenessGraceCheck(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": httpProbe(int64(8080), int64(0)),
"startupProbe": map[string]any{"httpGet": map[string]any{"port": int64(8080)}},
}))))
if findingCount(t, found, "liveness-without-startup-grace") != 0 {
t.Fatal("a startupProbe did not satisfy the grace check")
}
}
func TestAJobIsToldApartFromADeploymentOnRestartPolicy(t *testing.T) {
job := workload("Job", "import", podSpecWith(map[string]any{
"restartPolicy": "OnFailure",
}, container("app", nil)))
if findingCount(t, report(t, job), "wrong-restart-policy") != 0 {
t.Fatal("OnFailure was reported as wrong for a Job")
}
always := workload("Job", "import", podSpecWith(map[string]any{
"restartPolicy": alwaysPull,
}, container("app", nil)))
if !strings.Contains(onlyFinding(t, report(t, always), "wrong-restart-policy").Detail, "Job cannot use") {
t.Fatal("Always was not reported as wrong for a Job")
}
unset := workload("Job", "import", podSpec(container("app", nil)))
if !strings.Contains(onlyFinding(t, report(t, unset), "wrong-restart-policy").Detail, "unset") {
t.Fatal("an unset restartPolicy on a Job was not reported")
}
}
func TestNoPreStopIsNotAskedOfAJob(t *testing.T) {
found := report(t, workload("Job", "import", podSpecWith(map[string]any{
"restartPolicy": "Never",
}, container("app", nil))))
if findingCount(t, found, "no-prestop-hook") != 0 {
t.Fatal("a Job was asked for a preStop hook")
}
}
func TestEphemeralStorageSaysWhichHalfIsMissing(t *testing.T) {
requestOnly := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"resources": map[string]any{"requests": map[string]any{ephemeralName: "1Gi"}},
}))))
if detail := onlyFinding(t, requestOnly, "ephemeral-storage-unset").Detail; detail != "no ephemeral-storage limit" {
t.Fatalf("detail was %q, want the limit named", detail)
}
limitOnly := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"resources": map[string]any{"limits": map[string]any{ephemeralName: "1Gi"}},
}))))
if detail := onlyFinding(t, limitOnly, "ephemeral-storage-unset").Detail; detail != "no ephemeral-storage request" {
t.Fatalf("detail was %q, want the request named", detail)
}
}
func TestMemoryIsOnlyJudgedWhenBothNumbersExist(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"resources": map[string]any{
"requests": map[string]any{"memory": "128Mi", ephemeralName: "1Gi"},
"limits": map[string]any{ephemeralName: "1Gi"},
},
}))))
if findingCount(t, found, "memory-limit-not-request") != 0 {
t.Fatal("a container with no memory limit was judged on its QoS class")
}
}
func TestAPodAntiAffinitySatisfiesTheSpreadCheck(t *testing.T) {
found := report(t, settledDeployment(settledPod(map[string]any{
"topologySpreadConstraints": nil,
"affinity": map[string]any{"podAntiAffinity": map[string]any{}},
}, settledContainer(nil))))
if findingCount(t, found, "no-spread-no-anti-affinity") != 0 {
t.Fatal("a podAntiAffinity did not satisfy the spread check")
}
}
func TestASingleReplicaIsNotAskedToSpread(t *testing.T) {
obj := settledDeployment(settledPod(map[string]any{"topologySpreadConstraints": nil}, settledContainer(nil)))
found := report(t, replicas(obj, 1))
if findingCount(t, found, "no-spread-no-anti-affinity") != 0 {
t.Fatal("a single-replica deployment was asked to spread")
}
}
func TestMaxUnavailableIsReadAsBothACountAndAPercentage(t *testing.T) {
counted := withSpec(settled(), "strategy", map[string]any{
"rollingUpdate": map[string]any{"maxUnavailable": int64(2)},
})
if findingCount(t, report(t, counted), "max-unavailable-all") != 1 {
t.Fatal("maxUnavailable equal to the replica count was not reported")
}
partial := withSpec(settled(), "strategy", map[string]any{
"rollingUpdate": map[string]any{"maxUnavailable": "50%"},
})
if findingCount(t, report(t, partial), "max-unavailable-all") != 0 {
t.Fatal("maxUnavailable of half the workload was reported as all of it")
}
}
func TestAStatefulSetThatNamesItsServiceIsAccepted(t *testing.T) {
obj := workload("StatefulSet", "db", settledPod(nil, settledContainer(nil)))
found := report(t, withSpec(obj, "serviceName", "db"))
if findingCount(t, found, "statefulset-no-service-name") != 0 {
t.Fatal("a StatefulSet that names its service was reported")
}
}
func TestTheRevisionHistoryCheckNamesTheCountAndLetsTheDefaultAlone(t *testing.T) {
high := report(t, withSpec(settled(), revisionHistory, int64(50)))
if !strings.Contains(onlyFinding(t, high, "unbounded-revision-history").Detail, "50 old revisions") {
t.Fatal("a high revisionHistoryLimit did not name the count")
}
unset := report(t, withSpec(settled(), revisionHistory, nil))
if findingCount(t, unset, "unbounded-revision-history") != 0 {
t.Fatal("an unset revisionHistoryLimit was reported, but a live apiserver always defaults it to ten")
}
}
func TestOnlyTheRepeatedEnvKeyIsNamedAndOnlyOnce(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"env": []any{
map[string]any{"name": "MODE", "value": "a"},
map[string]any{"name": "MODE", "value": "b"},
map[string]any{"name": "MODE", "value": "c"},
map[string]any{"name": "PORT", "value": "8080"},
},
}))))
if detail := onlyFinding(t, found, "duplicate-env-keys").Detail; detail != "MODE is set more than once" {
t.Fatalf("detail was %q, want MODE named once", detail)
}
}
func TestListsAndFieldsOfTheWrongShapeAreSkippedByTheLifecycleChecks(t *testing.T) {
found := report(t, settledDeployment(settledPod(map[string]any{
"topologySpreadConstraints": nil,
"affinity": "not-an-object",
}, settledContainer(map[string]any{
"ports": []any{"not-an-object"},
"env": []any{"not-an-object", map[string]any{"value": "no name"}},
"lifecycle": "not-an-object",
"livenessProbe": map[string]any{"exec": map[string]any{"command": []any{"true"}}},
}))))
if findingCount(t, found, "duplicate-env-keys") != 0 {
t.Fatal("an env list of non-objects produced a duplicate")
}
if findingCount(t, found, "probe-port-not-declared") != 0 {
t.Fatal("a ports list of non-objects was read as declaring ports")
}
if findingCount(t, found, "no-prestop-hook") != 1 {
t.Fatal("a lifecycle field of the wrong shape was read as holding a preStop hook")
}
if findingCount(t, found, "no-spread-no-anti-affinity") != 1 {
t.Fatal("an affinity field of the wrong shape was read as anti-affinity")
}
}
func TestPortsAndProbeValuesDecodedAsFloatsAreStillRead(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"ports": []any{map[string]any{"containerPort": float64(8080)}},
"livenessProbe": map[string]any{"tcpSocket": map[string]any{"port": float64(9090)}},
"readinessProbe": map[string]any{"tcpSocket": map[string]any{"port": float64(8080)}},
}))))
if !strings.Contains(onlyFinding(t, found, "probe-port-not-declared").Detail, "9090") {
t.Fatal("a float-decoded probe port was not compared against the declared ports")
}
}
func TestTwoIdenticalTcpProbesAreCaught(t *testing.T) {
same := map[string]any{"tcpSocket": map[string]any{"port": int64(8080), "host": "localhost"}}
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": same,
"readinessProbe": map[string]any{"tcpSocket": map[string]any{"port": int64(8080), "host": "localhost"}},
}))))
if !strings.Contains(onlyFinding(t, found, "probes-identical").Detail, "tcpSocket") {
t.Fatal("two identical tcp probes were not caught")
}
}
func TestProbesHoldingValuesOfDifferentTypesAreNotIdentical(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": map[string]any{"httpGet": map[string]any{"port": int64(8080), "scheme": "HTTP"}},
"readinessProbe": map[string]any{"httpGet": map[string]any{"port": int64(8080), "scheme": true}},
}))))
if findingCount(t, found, "probes-identical") != 0 {
t.Fatal("probes whose fields hold different types were called identical")
}
}
func TestProbesWithDifferentHTTPHeadersAreNotIdentical(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": map[string]any{"httpGet": map[string]any{
"port": int64(8080),
"httpHeaders": []any{map[string]any{"name": "X-Probe", "value": "live"}},
}},
"readinessProbe": map[string]any{"httpGet": map[string]any{
"port": int64(8080),
"httpHeaders": []any{map[string]any{"name": "X-Probe", "value": "ready"}},
}},
}))))
if findingCount(t, found, "probes-identical") != 0 {
t.Fatal("probes with different HTTP headers were called identical")
}
}
func TestAProbeWithNoRecognisedHandlerIsIgnored(t *testing.T) {
found := report(t, settledDeployment(settledPod(nil, settledContainer(map[string]any{
"livenessProbe": map[string]any{"initialDelaySeconds": int64(5)},
"readinessProbe": map[string]any{"initialDelaySeconds": int64(5)},
}))))
if findingCount(t, found, "probes-identical") != 0 {
t.Fatal("two probes with no handler at all were called identical")
}
if findingCount(t, found, "probe-port-not-declared") != 0 {
t.Fatal("a probe with no handler was read as naming a port")
}
}
func TestMaxUnavailableOfAShapeNobodyWritesIsIgnored(t *testing.T) {
odd := withSpec(settled(), "strategy", map[string]any{
"rollingUpdate": map[string]any{"maxUnavailable": true},
})
if findingCount(t, report(t, odd), "max-unavailable-all") != 0 {
t.Fatal("a boolean maxUnavailable was read as a count")
}
unparsable := withSpec(settled(), "strategy", map[string]any{
"rollingUpdate": map[string]any{"maxUnavailable": "half"},
})
if findingCount(t, report(t, unparsable), "max-unavailable-all") != 0 {
t.Fatal("a maxUnavailable that is not a number was read as one")
}
}
func TestAStrategyWithNoRollingUpdateBlockIsIgnored(t *testing.T) {
found := report(t, withSpec(settled(), "strategy", map[string]any{"type": "RollingUpdate"}))
if findingCount(t, found, "max-unavailable-all") != 0 {
t.Fatal("a strategy with no rollingUpdate block was reported")
}
}
func TestARollingUpdateWithDefaultAvailabilityIsIgnored(t *testing.T) {
found := report(t, withSpec(settled(), "strategy", map[string]any{
"rollingUpdate": map[string]any{"maxSurge": "25%"},
}))
if findingCount(t, found, "max-unavailable-all") != 0 {
t.Fatal("an unset maxUnavailable was reported")
}
}
func TestABarePodThatRunsToCompletionKeepsItsRestartPolicy(t *testing.T) {
oneShot := pod("importer", podSpecWith(map[string]any{
"restartPolicy": "Never",
}, container("app", nil)))
if findingCount(t, report(t, oneShot), "wrong-restart-policy") != 0 {
t.Fatal("a bare pod using Never was told to use Always")
}
}
func TestADeploymentThatRefusesToRestartIsStillReported(t *testing.T) {
stubborn := deployment("api", podSpecWith(map[string]any{
"restartPolicy": "Never",
}, container("app", nil)))
if findingCount(t, report(t, stubborn), "wrong-restart-policy") != 1 {
t.Fatal("a Deployment refusing to restart its pods was not reported")
}
}