152 lines
4.0 KiB
TypeScript
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");
|
|
});
|
|
});
|