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.
This commit is contained in:
1 parent
797f5ce01a
commit
fea9940774
5 files changed
+246
-2
No files matched your search
@@ -3116,6 +3116,38 @@ export async function containerRoutes(app: FastifyInstance) {
|
||||
body.data.content,
|
||||
);
|
||||
|
||||
const deploymentPath =
|
||||
container.deploymentSource?.deploymentPath?.trim();
|
||||
if (
|
||||
deploymentPath &&
|
||||
isGitRedeploySourceType(container.sourceType)
|
||||
) {
|
||||
const persistentEnvPath = normalizeDeploymentChildPath(
|
||||
deploymentPath,
|
||||
container.deploymentSource?.buildPath,
|
||||
);
|
||||
|
||||
if (!persistentEnvPath) {
|
||||
throw new Error(
|
||||
"The active container .env was updated, but its Git deployment path could not be validated for rebuild persistence",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await ssh.writeDeploymentEnvFile(
|
||||
container.server,
|
||||
persistentEnvPath,
|
||||
body.data.content,
|
||||
);
|
||||
} catch (error) {
|
||||
const cause =
|
||||
error instanceof Error ? error.message : "unknown error";
|
||||
throw new Error(
|
||||
`The active container .env was updated, but Doktainer could not persist it for the next rebuild: ${cause}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await auditLog({
|
||||
userId: req.userId,
|
||||
serverId: container.serverId,
|
||||
@@ -4983,8 +5015,13 @@ export async function containerRoutes(app: FastifyInstance) {
|
||||
);
|
||||
}
|
||||
const prepared = gitDeploymentResult.preparedRuntime;
|
||||
const targetRuntime: RuntimeReplacementSpec = {
|
||||
const recoverablePreviousRuntime: RuntimeReplacementSpec = {
|
||||
...previousRuntime,
|
||||
envFilePath:
|
||||
prepared.envFilePath ?? previousRuntime.envFilePath,
|
||||
};
|
||||
const targetRuntime: RuntimeReplacementSpec = {
|
||||
...recoverablePreviousRuntime,
|
||||
...prepared,
|
||||
networks: Array.from(
|
||||
new Set([
|
||||
@@ -5016,7 +5053,7 @@ export async function containerRoutes(app: FastifyInstance) {
|
||||
deploymentId: runningDeployment.id,
|
||||
operationLabel: "rebuild",
|
||||
targetRuntime,
|
||||
previousRuntime,
|
||||
previousRuntime: recoverablePreviousRuntime,
|
||||
plan,
|
||||
finalize: finalizeGitRuntime,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ export type RuntimeReplacementSpec = {
|
||||
image: string;
|
||||
ports: string;
|
||||
env: string;
|
||||
envFilePath?: string;
|
||||
volumes: string;
|
||||
network: string;
|
||||
networks: string[];
|
||||
@@ -209,6 +210,7 @@ function snapshotRuntime(
|
||||
image,
|
||||
ports: normalizeRollbackPortMappings(snapshot.ports),
|
||||
env: snapshotString(snapshot, "env"),
|
||||
envFilePath: snapshotString(snapshot, "envFilePath") || undefined,
|
||||
volumes: snapshotString(snapshot, "volumes"),
|
||||
network,
|
||||
networks: normalizeRuntimeNetworks(network, snapshot.networks),
|
||||
@@ -422,6 +424,7 @@ function validateRuntimeBeforeMutation(input: {
|
||||
image: input.runtime.image,
|
||||
ports: input.ports,
|
||||
env: input.runtime.env,
|
||||
envFilePath: input.runtime.envFilePath,
|
||||
volumes: input.runtime.volumes,
|
||||
network: input.runtime.network,
|
||||
cpuLimit: input.runtime.cpuLimit,
|
||||
@@ -554,6 +557,7 @@ export async function replaceRuntimeForRollback<TResult = undefined>(
|
||||
image: input.targetRuntime.image,
|
||||
ports: targetPorts,
|
||||
env: input.targetRuntime.env,
|
||||
envFilePath: input.targetRuntime.envFilePath,
|
||||
volumes: input.targetRuntime.volumes,
|
||||
network: input.targetRuntime.network,
|
||||
cpuLimit: input.targetRuntime.cpuLimit,
|
||||
@@ -664,6 +668,7 @@ export async function replaceRuntimeForRollback<TResult = undefined>(
|
||||
image: input.previousRuntime.image,
|
||||
ports: input.previousRuntime.ports,
|
||||
env: input.previousRuntime.env,
|
||||
envFilePath: input.previousRuntime.envFilePath,
|
||||
volumes: input.previousRuntime.volumes,
|
||||
network: input.previousRuntime.network,
|
||||
cpuLimit: input.previousRuntime.cpuLimit,
|
||||
@@ -839,6 +844,7 @@ export async function replaceRuntimeWithProxyCandidate<TResult>(input: {
|
||||
image: input.targetRuntime.image,
|
||||
ports: candidatePorts,
|
||||
env: input.targetRuntime.env,
|
||||
envFilePath: input.targetRuntime.envFilePath,
|
||||
volumes: input.targetRuntime.volumes,
|
||||
network: input.targetRuntime.network,
|
||||
cpuLimit: input.targetRuntime.cpuLimit,
|
||||
@@ -917,6 +923,7 @@ export async function replaceRuntimeWithProxyCandidate<TResult>(input: {
|
||||
image: input.targetRuntime.image,
|
||||
ports: input.targetRuntime.ports,
|
||||
env: input.targetRuntime.env,
|
||||
envFilePath: input.targetRuntime.envFilePath,
|
||||
volumes: input.targetRuntime.volumes,
|
||||
network: input.targetRuntime.network,
|
||||
cpuLimit: input.targetRuntime.cpuLimit,
|
||||
@@ -1071,6 +1078,7 @@ export async function replaceRuntimeWithProxyCandidate<TResult>(input: {
|
||||
image: input.previousRuntime.image,
|
||||
ports: input.previousRuntime.ports,
|
||||
env: input.previousRuntime.env,
|
||||
envFilePath: input.previousRuntime.envFilePath,
|
||||
volumes: input.previousRuntime.volumes,
|
||||
network: input.previousRuntime.network,
|
||||
cpuLimit: input.previousRuntime.cpuLimit,
|
||||
|
||||
@@ -199,6 +199,16 @@ function validateNetworkName(value?: string): string | null {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateRuntimeEnvFilePath(value?: string): string | null {
|
||||
const candidate = value?.trim();
|
||||
if (!candidate) return null;
|
||||
if (/\r|\n|\0/.test(candidate) || !pathPosix.isAbsolute(candidate)) {
|
||||
throw new Error("Runtime env file path must be an absolute path");
|
||||
}
|
||||
|
||||
return pathPosix.normalize(candidate);
|
||||
}
|
||||
|
||||
function parsePortNumber(value: string, label: string): number {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
throw new Error(`${label} must be numeric`);
|
||||
@@ -443,6 +453,7 @@ export function buildDockerRunCommand(opts: {
|
||||
image: string;
|
||||
ports?: string;
|
||||
env?: string;
|
||||
envFilePath?: string;
|
||||
restartPolicy: string;
|
||||
volumes?: string;
|
||||
network?: string;
|
||||
@@ -503,6 +514,11 @@ export function buildDockerRunCommand(opts: {
|
||||
args.push("-e", envAssignment);
|
||||
}
|
||||
|
||||
const envFilePath = validateRuntimeEnvFilePath(opts.envFilePath);
|
||||
if (envFilePath) {
|
||||
args.push("--env-file", envFilePath);
|
||||
}
|
||||
|
||||
for (const volumeMount of parseVolumeMounts(
|
||||
opts.volumes,
|
||||
opts.mountValidation,
|
||||
@@ -965,6 +981,7 @@ export type PreparedGitRuntime = {
|
||||
image: string;
|
||||
ports: string;
|
||||
env: string;
|
||||
envFilePath?: string;
|
||||
volumes: string;
|
||||
network: string;
|
||||
restartPolicy: string;
|
||||
@@ -1699,6 +1716,7 @@ export async function runContainer(
|
||||
image: string;
|
||||
ports?: string; // "80:80,443:443"
|
||||
env?: string; // "KEY=val\nKEY2=val2"
|
||||
envFilePath?: string;
|
||||
restartPolicy: string;
|
||||
volumes?: string;
|
||||
network?: string;
|
||||
@@ -2452,6 +2470,7 @@ async function bootstrapSourceProject(
|
||||
deploymentPath: string;
|
||||
buildPath?: string;
|
||||
sourceProject: Awaited<ReturnType<typeof inspectSourceProject>>;
|
||||
managedEnvFilePath?: string;
|
||||
},
|
||||
) {
|
||||
const buildPath = normalizeBuildSubdirectory(opts.buildPath);
|
||||
@@ -2472,6 +2491,7 @@ async function bootstrapSourceProject(
|
||||
`DEPLOY_PATH=${escapeShellArg(opts.deploymentPath)}`,
|
||||
`BUILD_PATH=${escapeShellArg(buildPath)}`,
|
||||
`ENV_TEMPLATE=${escapeShellArg(templateToCopy)}`,
|
||||
`MANAGED_ENV_FILE=${escapeShellArg(opts.managedEnvFilePath ?? "")}`,
|
||||
'if [ "$BUILD_PATH" = "." ]; then TARGET_PATH="$DEPLOY_PATH"; else TARGET_PATH="$DEPLOY_PATH/$BUILD_PATH"; fi',
|
||||
'if [ ! -d "$TARGET_PATH" ]; then echo "Build path not found: $BUILD_PATH"; exit 1; fi',
|
||||
'if [ -n "$ENV_TEMPLATE" ] && [ ! -f "$TARGET_PATH/.env" ] && [ -f "$TARGET_PATH/$ENV_TEMPLATE" ]; then cp "$TARGET_PATH/$ENV_TEMPLATE" "$TARGET_PATH/.env"; fi',
|
||||
@@ -2505,6 +2525,15 @@ async function bootstrapSourceProject(
|
||||
...(sourceProject.hasBootstrapCacheDir
|
||||
? ['chmod -R ugo+rwX "$TARGET_PATH/bootstrap/cache" || true']
|
||||
: []),
|
||||
'if [ -n "$MANAGED_ENV_FILE" ] && [ -f "$TARGET_PATH/.env" ]; then',
|
||||
' MANAGED_ENV_DIR=$(dirname "$MANAGED_ENV_FILE")',
|
||||
' mkdir -p "$MANAGED_ENV_DIR"',
|
||||
' chmod 700 "$MANAGED_ENV_DIR" || true',
|
||||
' TEMP_ENV="${MANAGED_ENV_FILE}.tmp.$$"',
|
||||
' cp "$TARGET_PATH/.env" "$TEMP_ENV"',
|
||||
' chmod 600 "$TEMP_ENV" || true',
|
||||
' mv -f "$TEMP_ENV" "$MANAGED_ENV_FILE"',
|
||||
"fi",
|
||||
].join("\n");
|
||||
|
||||
await execStrict(
|
||||
@@ -2747,6 +2776,11 @@ export async function deployContainerFromGitSource(
|
||||
`doktainer/${sanitizeProjectName(projectName)}:latest`;
|
||||
const runOverride = resolveRunOverride(opts.startCommand);
|
||||
const shouldApplyRuntimeOverride = buildType === "DOCKERFILE";
|
||||
const managedEnvDirectory = `${deploymentPath}.doktainer`;
|
||||
const managedRuntimeEnvFilePath = pathPosix.join(
|
||||
managedEnvDirectory,
|
||||
buildPath === "." ? "root.env" : "build.env",
|
||||
);
|
||||
|
||||
const cloneCommand = [
|
||||
"git",
|
||||
@@ -2764,9 +2798,47 @@ export async function deployContainerFromGitSource(
|
||||
"set -euo pipefail",
|
||||
"export GIT_TERMINAL_PROMPT=0",
|
||||
`DEPLOY_PATH=${escapeShellArg(deploymentPath)}`,
|
||||
`BUILD_PATH=${escapeShellArg(buildPath)}`,
|
||||
`MANAGED_ENV_DIR=${escapeShellArg(managedEnvDirectory)}`,
|
||||
'ROOT_ENV="$DEPLOY_PATH/.env"',
|
||||
'if [ "$BUILD_PATH" = "." ]; then TARGET_ENV="$ROOT_ENV"; else TARGET_ENV="$DEPLOY_PATH/$BUILD_PATH/.env"; fi',
|
||||
'ROOT_MANAGED_ENV="$MANAGED_ENV_DIR/root.env"',
|
||||
'if [ "$BUILD_PATH" = "." ]; then TARGET_MANAGED_ENV="$ROOT_MANAGED_ENV"; else TARGET_MANAGED_ENV="$MANAGED_ENV_DIR/build.env"; fi',
|
||||
"persist_env() {",
|
||||
' SOURCE_ENV="$1"; MANAGED_ENV="$2"',
|
||||
' if [ ! -f "$SOURCE_ENV" ]; then return; fi',
|
||||
' mkdir -p "$MANAGED_ENV_DIR"',
|
||||
' chmod 700 "$MANAGED_ENV_DIR" || true',
|
||||
' TEMP_ENV="${MANAGED_ENV}.tmp.$$"',
|
||||
' cp "$SOURCE_ENV" "$TEMP_ENV"',
|
||||
' chmod 600 "$TEMP_ENV" || true',
|
||||
' mv -f "$TEMP_ENV" "$MANAGED_ENV"',
|
||||
"}",
|
||||
"restore_env() {",
|
||||
' MANAGED_ENV="$1"; DESTINATION_ENV="$2"',
|
||||
' if [ ! -f "$MANAGED_ENV" ]; then return; fi',
|
||||
' mkdir -p "$(dirname "$DESTINATION_ENV")"',
|
||||
' cp "$MANAGED_ENV" "$DESTINATION_ENV"',
|
||||
' chmod 600 "$DESTINATION_ENV" || true',
|
||||
"}",
|
||||
'mkdir -p "$(dirname "$DEPLOY_PATH")"',
|
||||
'persist_env "$ROOT_ENV" "$ROOT_MANAGED_ENV"',
|
||||
'if [ "$TARGET_ENV" != "$ROOT_ENV" ]; then persist_env "$TARGET_ENV" "$TARGET_MANAGED_ENV"; fi',
|
||||
'rm -rf "$DEPLOY_PATH"',
|
||||
cloneCommand,
|
||||
'restore_env "$ROOT_MANAGED_ENV" "$ROOT_ENV"',
|
||||
'if [ "$TARGET_ENV" != "$ROOT_ENV" ]; then restore_env "$TARGET_MANAGED_ENV" "$TARGET_ENV"; fi',
|
||||
'if [ ! -f "$TARGET_ENV" ]; then',
|
||||
' TARGET_DIR=$(dirname "$TARGET_ENV")',
|
||||
' if [ -f "$TARGET_DIR/.env.example" ]; then ENV_TEMPLATE="$TARGET_DIR/.env.example"',
|
||||
' elif [ -f "$TARGET_DIR/.env.dist" ]; then ENV_TEMPLATE="$TARGET_DIR/.env.dist"',
|
||||
' elif [ -f "$TARGET_DIR/env" ]; then ENV_TEMPLATE="$TARGET_DIR/env"',
|
||||
' else ENV_TEMPLATE=""',
|
||||
" fi",
|
||||
' if [ -n "$ENV_TEMPLATE" ]; then cp "$ENV_TEMPLATE" "$TARGET_ENV"; chmod 600 "$TARGET_ENV" || true; fi',
|
||||
"fi",
|
||||
'persist_env "$ROOT_ENV" "$ROOT_MANAGED_ENV"',
|
||||
'if [ "$TARGET_ENV" != "$ROOT_ENV" ]; then persist_env "$TARGET_ENV" "$TARGET_MANAGED_ENV"; fi',
|
||||
].join("\n");
|
||||
|
||||
try {
|
||||
@@ -2841,6 +2913,20 @@ export async function deployContainerFromGitSource(
|
||||
};
|
||||
}
|
||||
|
||||
const hasManagedRuntimeEnvFile = (
|
||||
await execStrict(
|
||||
server,
|
||||
privilegedCommand(
|
||||
server,
|
||||
`bash -lc ${escapeShellArg(`if [ -f ${escapeShellArg(managedRuntimeEnvFilePath)} ]; then printf 1; else printf 0; fi`)}`,
|
||||
),
|
||||
shortDockerCommandTimeout(DOCKER_INSPECT_TIMEOUT_MS),
|
||||
)
|
||||
).trim() === "1";
|
||||
const runtimeEnvFilePath = hasManagedRuntimeEnvFile
|
||||
? managedRuntimeEnvFilePath
|
||||
: undefined;
|
||||
|
||||
if (buildType === "NIXPACKS") {
|
||||
const sourceProject = await inspectSourceProject(server, {
|
||||
deploymentPath,
|
||||
@@ -2850,6 +2936,7 @@ export async function deployContainerFromGitSource(
|
||||
deploymentPath,
|
||||
buildPath,
|
||||
sourceProject,
|
||||
managedEnvFilePath: runtimeEnvFilePath,
|
||||
});
|
||||
if (
|
||||
sourceProject.likelyLaravel &&
|
||||
@@ -2922,6 +3009,7 @@ export async function deployContainerFromGitSource(
|
||||
image: imageTag,
|
||||
ports: autoPorts ?? "",
|
||||
env: runtimeEnv ?? "",
|
||||
envFilePath: runtimeEnvFilePath,
|
||||
restartPolicy: opts.restartPolicy?.trim() || "unless-stopped",
|
||||
volumes: opts.volumes ?? "",
|
||||
network: opts.network?.trim() || "bridge",
|
||||
@@ -2958,6 +3046,7 @@ export async function deployContainerFromGitSource(
|
||||
deploymentPath,
|
||||
buildPath,
|
||||
sourceProject,
|
||||
managedEnvFilePath: runtimeEnvFilePath,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -3041,6 +3130,7 @@ export async function deployContainerFromGitSource(
|
||||
image: imageTag,
|
||||
ports: autoPorts ?? "",
|
||||
env: runtimeEnv ?? "",
|
||||
envFilePath: runtimeEnvFilePath,
|
||||
restartPolicy: opts.restartPolicy?.trim() || "unless-stopped",
|
||||
volumes: opts.volumes ?? "",
|
||||
network: opts.network?.trim() || "bridge",
|
||||
@@ -3081,6 +3171,7 @@ export async function deployContainerFromGitSource(
|
||||
image: imageTag,
|
||||
ports: autoPorts ?? "",
|
||||
env: opts.env ?? "",
|
||||
envFilePath: runtimeEnvFilePath,
|
||||
restartPolicy: opts.restartPolicy?.trim() || "unless-stopped",
|
||||
volumes: opts.volumes ?? "",
|
||||
network: opts.network?.trim() || "bridge",
|
||||
@@ -3154,6 +3245,7 @@ export async function deployContainerFromGitSource(
|
||||
image: imageTag,
|
||||
ports: autoPorts ?? "",
|
||||
env: opts.env ?? "",
|
||||
envFilePath: runtimeEnvFilePath,
|
||||
restartPolicy: opts.restartPolicy?.trim() || "unless-stopped",
|
||||
volumes: opts.volumes ?? "",
|
||||
network: opts.network?.trim() || "bridge",
|
||||
|
||||
@@ -103,6 +103,88 @@ test("Git deployment resolves and returns the cloned commit SHA", () => {
|
||||
assert.match(gitDeployFunction, /preparedRuntime/);
|
||||
});
|
||||
|
||||
test("Git rebuild preserves the project env outside the replaceable checkout", () => {
|
||||
const source = readSource(DOCKER_CONTAINERS);
|
||||
const start = source.indexOf(
|
||||
"export async function deployContainerFromGitSource",
|
||||
);
|
||||
assert.notEqual(start, -1);
|
||||
const gitDeployFunction = source.slice(start);
|
||||
|
||||
const persistIndex = gitDeployFunction.indexOf(
|
||||
'persist_env "$ROOT_ENV" "$ROOT_MANAGED_ENV"',
|
||||
);
|
||||
const removeIndex = gitDeployFunction.indexOf('rm -rf "$DEPLOY_PATH"');
|
||||
const cloneIndex = gitDeployFunction.indexOf("cloneCommand,", removeIndex);
|
||||
const restoreIndex = gitDeployFunction.indexOf(
|
||||
'restore_env "$ROOT_MANAGED_ENV" "$ROOT_ENV"',
|
||||
);
|
||||
const templateIndex = gitDeployFunction.indexOf(
|
||||
'if [ -f "$TARGET_DIR/.env.example" ]',
|
||||
);
|
||||
|
||||
assert.ok(persistIndex >= 0);
|
||||
assert.ok(removeIndex > persistIndex);
|
||||
assert.ok(cloneIndex > removeIndex);
|
||||
assert.ok(restoreIndex > cloneIndex);
|
||||
assert.ok(templateIndex > restoreIndex);
|
||||
assert.match(gitDeployFunction, /managedEnvDirectory = `\$\{deploymentPath\}\.doktainer`/);
|
||||
assert.match(gitDeployFunction, /chmod 700 .*MANAGED_ENV_DIR/);
|
||||
assert.match(gitDeployFunction, /chmod 600 .*TEMP_ENV/);
|
||||
assert.match(gitDeployFunction, /BUILD_PATH/);
|
||||
assert.match(gitDeployFunction, /envFilePath: runtimeEnvFilePath/);
|
||||
});
|
||||
|
||||
test("Git container env edits are mirrored to the deployment checkout", () => {
|
||||
const source = readSource(CONTAINER_ROUTES);
|
||||
const readRouteIndex = source.indexOf('"/:id/project-env"');
|
||||
const writeRouteIndex = source.indexOf(
|
||||
'"/:id/project-env"',
|
||||
readRouteIndex + 1,
|
||||
);
|
||||
const nextRouteIndex = source.indexOf('"/:id/exec"', writeRouteIndex);
|
||||
assert.ok(readRouteIndex >= 0);
|
||||
assert.ok(writeRouteIndex > readRouteIndex);
|
||||
assert.ok(nextRouteIndex > writeRouteIndex);
|
||||
const envRoute = source.slice(writeRouteIndex, nextRouteIndex);
|
||||
const containerWriteIndex = envRoute.indexOf("ssh.writeContainerFile");
|
||||
const gitSourceCheckIndex = envRoute.indexOf(
|
||||
"isGitRedeploySourceType(container.sourceType)",
|
||||
);
|
||||
const deploymentWriteIndex = envRoute.indexOf(
|
||||
"ssh.writeDeploymentEnvFile",
|
||||
containerWriteIndex,
|
||||
);
|
||||
|
||||
assert.ok(containerWriteIndex >= 0);
|
||||
assert.ok(gitSourceCheckIndex > containerWriteIndex);
|
||||
assert.ok(deploymentWriteIndex > gitSourceCheckIndex);
|
||||
assert.match(envRoute, /normalizeDeploymentChildPath/);
|
||||
assert.match(
|
||||
envRoute,
|
||||
/active container \.env was updated, but Doktainer could not persist it for the next rebuild/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Git rebuild applies the managed env file to target and recovery runtimes", () => {
|
||||
const source = readSource(CONTAINER_ROUTES);
|
||||
const rebuildRoute = sourceBlock(
|
||||
source,
|
||||
'"/:id/rebuild"',
|
||||
"GET /containers/:id/logs",
|
||||
);
|
||||
|
||||
assert.match(rebuildRoute, /recoverablePreviousRuntime/);
|
||||
assert.match(
|
||||
rebuildRoute,
|
||||
/prepared\.envFilePath \?\? previousRuntime\.envFilePath/,
|
||||
);
|
||||
assert.match(
|
||||
rebuildRoute,
|
||||
/previousRuntime: recoverablePreviousRuntime/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Git rebuild image tags are immutable and preserve registry ports", () => {
|
||||
assert.equal(
|
||||
toImmutableBuildImageTag(
|
||||
|
||||
@@ -20,6 +20,31 @@ describe("deployment strategy", () => {
|
||||
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");
|
||||
|
||||
Reference in new issue
Block a user