Files
kuber/tests/lib/rollout.test.ts
2026-09-27 10:40:50 +00:00

152 lines
4.0 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type {
V1Deployment,
V1Pod,
V1ReplicaSet,
} from "@kubernetes/client-node";
import {
currentReplicaSet,
rolloutPodFailure,
waitForRollout,
} from "../../lib/rollout";
const deployment = {
metadata: { name: "web", uid: "deployment-1", generation: 2 },
spec: {
replicas: 1,
template: {
metadata: { labels: { app: "web" } },
spec: { containers: [{ name: "app", image: "new:v2" }] },
},
},
status: {
observedGeneration: 2,
updatedReplicas: 1,
availableReplicas: 0,
unavailableReplicas: 1,
},
} as unknown as V1Deployment;
const set = (uid: string, image: string) =>
({
metadata: {
uid,
ownerReferences: [{ kind: "Deployment", uid: "deployment-1" }],
},
spec: {
template: {
metadata: { labels: { app: "web", "pod-template-hash": uid } },
spec: { containers: [{ name: "app", image }] },
},
},
}) as unknown as V1ReplicaSet;
const pod = (uid: string, status: object, init = false) =>
({
metadata: {
name: `${uid}-pod`,
ownerReferences: [{ kind: "ReplicaSet", uid }],
},
status: {
[init ? "initContainerStatuses" : "containerStatuses"]: [
{ name: "app", restartCount: 3, ...status },
],
},
}) as unknown as V1Pod;
describe("rollout failure diagnosis", () => {
test("ignores crashing pods from the previous ReplicaSet", () => {
const current = currentReplicaSet(deployment, [
set("old", "old:v1"),
set("new", "new:v2"),
]);
expect(current?.metadata?.uid).toBe("new");
expect(
rolloutPodFailure(deployment, current!, [
pod("old", { state: { waiting: { reason: "CrashLoopBackOff" } } }),
]),
).toBeUndefined();
expect(
currentReplicaSet(deployment, [set("old", "old:v1")]),
).toBeUndefined();
});
test("reports current init and application container failures with safe identifiers", () => {
const current = set("new", "new:v2");
expect(
rolloutPodFailure(deployment, current, [
pod(
"new",
{ state: { waiting: { reason: "ImagePullBackOff" } } },
true,
),
]),
).toContain("pod new-pod, container app: ImagePullBackOff");
expect(
rolloutPodFailure(deployment, current, [
pod("new", {
lastState: { terminated: { exitCode: 137, reason: "OOMKilled" } },
}),
]),
).toContain("OOMKilled (exit 137, restarts 3)");
expect(
rolloutPodFailure(deployment, current, [
pod("new", {
lastState: { terminated: { exitCode: 1, reason: "Error" } },
}),
]),
).toContain("Error (exit 1, restarts 3)");
});
test("detects deadline condition without waiting for pod listing", async () => {
const failed = {
...deployment,
status: {
...deployment.status,
conditions: [
{
type: "Progressing",
status: "False",
reason: "ProgressDeadlineExceeded",
},
],
},
} as V1Deployment;
await expect(
waitForRollout(
"web",
Date.now() + 100,
async () => failed,
async () => {
throw Error("should not list");
},
async () => [],
),
).rejects.toThrow(
"Deployment web rollout failed: ProgressDeadlineExceeded",
);
});
test("bounds hung reads by deadline and responds to cancellation", async () => {
const never = async (): Promise<V1Deployment> => new Promise(() => {});
await expect(
waitForRollout(
"web",
Date.now() + 20,
never,
async () => [],
async () => [],
),
).rejects.toThrow("Timed out waiting for deployment web rollout");
const controller = new AbortController();
const waiting = waitForRollout(
"web",
Date.now() + 10_000,
never,
async () => [],
async () => [],
controller.signal,
);
controller.abort();
await expect(waiting).rejects.toThrow("cancelled");
});
});