Files
doktainer-upstream/tests/backend/deployment-strategy.test.ts
T
KodekaTeam fea9940774 feat: Implement .env persistence during Git rebuilds
- Added functionality to persist `.env` files to a private sidecar directory before Git checkout replacement.
- Enhanced recovery of `.env` files from the sidecar after cloning, with fallback to templates if necessary.
- Introduced validation for `envFilePath` to ensure it is an absolute path and integrated it into Docker run commands.
- Updated deployment routes and rollback services to support the new environment file handling.
- Added regression tests to verify the persistence and recovery of environment variables across rebuilds.
2026-09-03 23:49:14 +07:00

136 lines
4.1 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("applies a managed env file without embedding its contents in the command", () => {
const command = buildDockerRunCommand({
name: "app",
image: "example/app:stable",
restartPolicy: "unless-stopped",
envFilePath: "/opt/doktainer/deployments/app.doktainer/root.env",
});
assert.match(
command,
/'--env-file' '\/opt\/doktainer\/deployments\/app\.doktainer\/root\.env'/,
);
assert.doesNotMatch(command, /SECRET_VALUE/);
assert.throws(
() =>
buildDockerRunCommand({
name: "app",
image: "example/app:stable",
restartPolicy: "unless-stopped",
envFilePath: "relative/.env",
}),
/absolute path/,
);
});
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/);
});
});