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

111 lines
3.4 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
resolveDeploymentStrategy,
resolveSafeRedeployStrategy,
} from "../../src/server/services/deployment-strategy";
import { buildDockerRunCommand } from "../../src/server/services/ssh-services/docker-containers";
describe("deployment strategy", () => {
it("preserves CPU and memory limits in Docker run commands", () => {
const command = buildDockerRunCommand({
name: "app",
image: "example/app:stable",
restartPolicy: "unless-stopped",
cpuLimit: 1.5,
memoryLimitMb: 512,
});
assert.match(command, /'--cpus' '1\.5'/);
assert.match(command, /'--memory' '512m'/);
});
it("uses atomic rename when no published ports are configured", () => {
assert.equal(resolveDeploymentStrategy(""), "ATOMIC_RENAME");
assert.equal(resolveDeploymentStrategy(" "), "ATOMIC_RENAME");
});
it("uses recreate with recovery when published ports are configured", () => {
assert.equal(resolveDeploymentStrategy("8080:80"), "RECREATE_WITH_RECOVERY");
});
});
describe("safe redeploy strategy", () => {
it("allows Docker to allocate candidate ports on loopback only", () => {
const command = buildDockerRunCommand({
name: "app-candidate",
image: "example/app:revision",
ports: "127.0.0.1::80",
restartPolicy: "unless-stopped",
});
assert.match(command, /127\.0\.0\.1::80/);
assert.throws(() =>
buildDockerRunCommand({
name: "unsafe-candidate",
image: "example/app:revision",
ports: "0.0.0.0::80",
restartPolicy: "unless-stopped",
}),
);
});
it("uses a proxy candidate for isolated managed domains", () => {
assert.deepEqual(
resolveSafeRedeployStrategy({
ports: "8080:80",
volumes: "",
domains: [
{ proxy: "NGINX", configMode: "ISOLATED", targetPort: 80 },
],
}),
{
strategy: "PROXY_CANDIDATE_SWITCH",
proxyTrafficContinuous: true,
directPortInterruptionPossible: true,
reason:
"Managed isolated proxies can switch to a validated candidate before the previous runtime is removed.",
},
);
});
it("keeps an honest direct-port fallback when a shared proxy cannot switch", () => {
const plan = resolveSafeRedeployStrategy({
ports: "8080:80",
volumes: "",
domains: [
{ proxy: "NGINX", configMode: "SHARED", targetPort: 80 },
],
});
assert.equal(plan.strategy, "RECREATE_WITH_RECOVERY");
assert.equal(plan.proxyTrafficContinuous, false);
assert.equal(plan.directPortInterruptionPossible, true);
});
it("blocks an internal-IP rename when a linked proxy cannot be switched", () => {
const plan = resolveSafeRedeployStrategy({
ports: "",
volumes: "",
domains: [
{ proxy: "NGINX", configMode: "SHARED", targetPort: 80 },
],
});
assert.equal(plan.strategy, "BLOCKED_UNSUPPORTED_PROXY");
});
it("does not overlap a candidate with a persistent volume", () => {
const plan = resolveSafeRedeployStrategy({
ports: "8080:80",
volumes: "app-data:/data",
domains: [
{ proxy: "NGINX", configMode: "ISOLATED", targetPort: 80 },
],
});
assert.equal(plan.strategy, "RECREATE_WITH_RECOVERY");
assert.match(plan.reason, /persistent storage/);
});
});