Files
doktainer-upstream/tests/backend/deployment-rollback-runtime.test.ts
T

276 lines
7.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
normalizeRollbackPortMappings,
replaceRuntimeForRollback,
resolveRollbackImageReference,
RollbackRuntimeError,
} from "../../src/server/services/deployment-rollback.service";
type RollbackInput = Parameters<typeof replaceRuntimeForRollback>[0];
type RollbackDependencies = NonNullable<
Parameters<typeof replaceRuntimeForRollback>[1]
>;
const server = {} as RollbackInput["server"];
test("normalizes Docker display port mappings for rollback", () => {
assert.equal(
normalizeRollbackPortMappings(
"0.0.0.0:8080->80/tcp, [::]:8080->80/tcp, 127.0.0.1:8443->443/tcp",
),
"8080:80,127.0.0.1:8443:443",
);
assert.equal(
normalizeRollbackPortMappings(["8081:81", "9000->90/udp"]),
"8081:81,9000:90/udp",
);
});
test("uses a locally available Docker image ID without creating an invalid registry reference", async () => {
const inspected: string[] = [];
const pulled: string[] = [];
const image = await resolveRollbackImageReference(
{
server,
image: "example/app:latest",
imageDigest: `sha256:${"a".repeat(64)}`,
},
{
dockerInspect: async (_server, imageRef) => {
inspected.push(imageRef);
return {};
},
dockerPullImage: async (_server, imageRef) => {
pulled.push(imageRef);
},
},
);
assert.equal(image, `sha256:${"a".repeat(64)}`);
assert.deepEqual(inspected, [`sha256:${"a".repeat(64)}`]);
assert.deepEqual(pulled, []);
});
test("falls back to the stored image when a historical local image ID was pruned", async () => {
const pulled: string[] = [];
const image = await resolveRollbackImageReference(
{
server,
image: "example/app:stable",
imageDigest: `sha256:${"b".repeat(64)}`,
},
{
dockerInspect: async () => {
throw new Error("image not found");
},
dockerPullImage: async (_server, imageRef) => {
pulled.push(imageRef);
},
},
);
assert.equal(image, "example/app:stable");
assert.deepEqual(pulled, ["example/app:stable"]);
});
function runtime(overrides: Partial<RollbackInput["targetRuntime"]> = {}) {
return {
image: "example/app:target",
ports: "",
env: "APP_ENV=production",
volumes: "/data:/data",
network: "bridge",
restartPolicy: "unless-stopped",
command: "",
...overrides,
};
}
function dependencies(
overrides: Partial<RollbackDependencies> = {},
): RollbackDependencies {
return {
runContainer: async () => "new-container-id",
stopAndRemoveContainer: async () => undefined,
dockerRename: async () => undefined,
waitForDockerHealth: async () => ({
healthy: true,
reason: "running",
status: "healthy",
attempts: 1,
durationMs: 0,
}),
...overrides,
};
}
test("published-port rollback removes the active runtime before claiming its name and host port", async () => {
const events: string[] = [];
await replaceRuntimeForRollback(
{
server,
containerName: "app",
currentContainerRefs: ["old-container-id", "app"],
temporaryName: "app-rollback",
strategy: "RECREATE_WITH_RECOVERY",
targetRuntime: runtime({ ports: "8080:80" }),
previousRuntime: runtime({
image: "example/app:previous",
ports: "8080:80",
}),
},
dependencies({
stopAndRemoveContainer: async (_server, refs) => {
events.push(`remove:${refs.join(",")}`);
},
runContainer: async (_server, options) => {
events.push(`run:${options.name}:${options.ports}`);
return "new-container-id";
},
waitForDockerHealth: async () => {
events.push("runtime-check");
return {
healthy: true,
reason: "running",
status: "healthy",
attempts: 1,
durationMs: 0,
};
},
}),
);
assert.deepEqual(events, [
"remove:old-container-id,app",
"run:app:8080:80",
"runtime-check",
]);
});
test("failed published-port rollback recreates and verifies the previous runtime", async () => {
const events: string[] = [];
let runCount = 0;
await assert.rejects(
replaceRuntimeForRollback(
{
server,
containerName: "app",
currentContainerRefs: ["old-container-id", "app"],
temporaryName: "app-rollback",
strategy: "RECREATE_WITH_RECOVERY",
targetRuntime: runtime({ ports: "8080:80" }),
previousRuntime: runtime({
image: "example/app:previous",
ports: "8080:80",
}),
},
dependencies({
stopAndRemoveContainer: async (_server, refs) => {
events.push(`remove:${refs.join(",")}`);
},
runContainer: async (_server, options) => {
runCount += 1;
events.push(`run:${options.image}`);
return runCount === 1 ? "failed-target-id" : "recovered-container-id";
},
waitForDockerHealth: async ({ containerRef }) => {
events.push(`runtime-check:${containerRef}`);
if (containerRef === "failed-target-id") {
return {
healthy: false,
reason: "not running",
status: "not_running",
attempts: 1,
durationMs: 0,
};
}
return {
healthy: true,
reason: "running",
status: "healthy",
attempts: 1,
durationMs: 0,
};
},
}),
),
(error: unknown) => {
assert.ok(error instanceof RollbackRuntimeError);
assert.equal(error.recoveryMode, "RECREATED");
assert.equal(error.recoveryDockerId, "recovered-container-id");
return true;
},
);
assert.deepEqual(events, [
"remove:old-container-id,app",
"run:example/app:target",
"runtime-check:failed-target-id",
"remove:failed-target-id,app-rollback,app",
"run:example/app:previous",
"runtime-check:recovered-container-id",
]);
});
test("failed atomic candidate leaves the active runtime in place", async () => {
const removedRefs: string[][] = [];
let runCount = 0;
await assert.rejects(
replaceRuntimeForRollback(
{
server,
containerName: "app",
currentContainerRefs: ["old-container-id", "app"],
temporaryName: "app-rollback",
strategy: "ATOMIC_RENAME",
targetRuntime: runtime(),
previousRuntime: runtime({ image: "example/app:previous" }),
},
dependencies({
stopAndRemoveContainer: async (_server, refs) => {
removedRefs.push(refs);
},
runContainer: async () => {
runCount += 1;
return "failed-target-id";
},
waitForDockerHealth: async ({ containerRef }) => {
if (containerRef === "failed-target-id") {
return {
healthy: false,
reason: "not running",
status: "not_running",
attempts: 1,
durationMs: 0,
};
}
return {
healthy: true,
reason: "running",
status: "healthy",
attempts: 1,
durationMs: 0,
};
},
}),
),
(error: unknown) => {
assert.ok(error instanceof RollbackRuntimeError);
assert.equal(error.recoveryMode, "UNCHANGED");
assert.equal(error.recoveryDockerId, "old-container-id");
return true;
},
);
assert.equal(runCount, 1);
assert.deepEqual(removedRefs, [["failed-target-id", "app-rollback"]]);
});