fix(docker): require bounded e2e docker commands

This commit is contained in:
Vincent Koc
2026-05-26 23:56:48 +02:00
parent 6729dea36f
commit 3736d7b60b
4 changed files with 275 additions and 13 deletions

View File

@@ -3,18 +3,29 @@
# Shared helpers for Docker E2E scripts that keep a named container running
# while polling readiness from the host.
docker_e2e_timeout_bin() {
if command -v timeout >/dev/null 2>&1; then
printf '%s\n' timeout
elif command -v gtimeout >/dev/null 2>&1; then
printf '%s\n' gtimeout
else
return 1
fi
}
docker_e2e_timeout_cmd() {
local timeout_value="$1"
shift
if command -v timeout >/dev/null 2>&1; then
if timeout --kill-after=1s 1s true >/dev/null 2>&1; then
timeout --kill-after=30s "$timeout_value" "$@"
else
timeout "$timeout_value" "$@"
fi
return
local timeout_bin
if ! timeout_bin="$(docker_e2e_timeout_bin)"; then
echo "timeout command not found; cannot bound Docker command after ${timeout_value}" >&2
return 127
fi
if "$timeout_bin" --kill-after=1s 1s true >/dev/null 2>&1; then
"$timeout_bin" --kill-after=30s "$timeout_value" "$@"
else
"$timeout_bin" "$timeout_value" "$@"
fi
"$@"
}
docker_e2e_docker_cmd() {

View File

@@ -20,15 +20,22 @@ if ! declare -F docker_e2e_docker_run_cmd >/dev/null 2>&1; then
return
fi
local timeout_value="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}"
local timeout_bin=""
if command -v timeout >/dev/null 2>&1; then
if timeout --kill-after=1s 1s true >/dev/null 2>&1; then
timeout --kill-after=30s "$timeout_value" docker "$@"
timeout_bin="timeout"
elif command -v gtimeout >/dev/null 2>&1; then
timeout_bin="gtimeout"
fi
if [ -n "$timeout_bin" ]; then
if "$timeout_bin" --kill-after=1s 1s true >/dev/null 2>&1; then
"$timeout_bin" --kill-after=30s "$timeout_value" docker "$@"
else
timeout "$timeout_value" docker "$@"
"$timeout_bin" "$timeout_value" docker "$@"
fi
return
fi
docker "$@"
echo "timeout command not found; cannot bound Docker run after ${timeout_value}" >&2
return 127
}
fi

View File

@@ -360,6 +360,8 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
"scripts/deadcode-unused-files.allowlist.mjs",
["test/scripts/check-deadcode-unused-files.test.ts"],
],
["scripts/lib/docker-e2e-container.sh", ["test/scripts/docker-build-helper.test.ts"]],
["scripts/lib/docker-e2e-package.sh", ["test/scripts/docker-build-helper.test.ts"]],
["scripts/lib/live-docker-stage.sh", ["test/scripts/live-docker-stage.test.ts"]],
["scripts/lib/openclaw-test-state.mjs", ["test/scripts/openclaw-test-state.test.ts"]],
["scripts/lib/vitest-local-scheduling.mjs", ["test/scripts/vitest-local-scheduling.test.ts"]],
@@ -420,6 +422,7 @@ const TOOLING_TEST_TARGETS = new Map([
["test/scripts/check-deadcode-unused-files.test.ts"],
],
["test/scripts/ci-docker-pull-retry.test.ts", ["test/scripts/ci-docker-pull-retry.test.ts"]],
["test/scripts/docker-build-helper.test.ts", ["test/scripts/docker-build-helper.test.ts"]],
["test/scripts/live-docker-stage.test.ts", ["test/scripts/live-docker-stage.test.ts"]],
["test/scripts/openclaw-test-state.test.ts", ["test/scripts/openclaw-test-state.test.ts"]],
[

View File

@@ -1,5 +1,13 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync,
mkdtempSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
@@ -244,6 +252,239 @@ grep -q '^pull openclaw-reuse-image$' "$TMPDIR/docker-seen"
}
});
it("fails Docker commands fast when timeout is unavailable", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-timeout-required-"));
try {
mkdirSync(join(workDir, "bin"));
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
export PATH="$TMPDIR/bin"
export DOCKER_COMMAND_TIMEOUT=7s
docker() {
printf "%s\\n" "$*" >"$TMPDIR/docker-seen"
}
export -f docker
source "$ROOT_DIR/scripts/lib/docker-e2e-container.sh"
set +e
docker_e2e_docker_cmd ps 2>"$TMPDIR/stderr"
status="$?"
set -e
stderr="$(<"$TMPDIR/stderr")"
[[ "$status" = "127" ]]
[[ "$stderr" = *"timeout command not found; cannot bound Docker command after 7s"* ]]
[[ ! -e "$TMPDIR/docker-seen" ]]
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("uses plain timeout when kill-after is unsupported", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-plain-timeout-"));
try {
const binDir = join(workDir, "bin");
mkdirSync(binDir);
writeFileSync(
join(binDir, "timeout"),
`#!/bin/bash
set -euo pipefail
if [[ "$1" = "--kill-after=1s" ]]; then
exit 1
fi
printf 'plain:%s|%s\\n' "$1" "\${*:2}" >>"$TMPDIR/timeout-seen"
shift
"$@"
`,
);
chmodSync(join(binDir, "timeout"), 0o755);
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
export PATH="$TMPDIR/bin:$PATH"
export DOCKER_COMMAND_TIMEOUT=9s
docker() {
printf "%s\\n" "$*" >>"$TMPDIR/docker-seen"
}
export -f docker
source "$ROOT_DIR/scripts/lib/docker-e2e-container.sh"
docker_e2e_docker_cmd image inspect demo
grep -q '^plain:9s|docker image inspect demo$' "$TMPDIR/timeout-seen"
grep -q '^image inspect demo$' "$TMPDIR/docker-seen"
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("uses gtimeout when timeout is unavailable", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-gtimeout-"));
try {
const binDir = join(workDir, "bin");
mkdirSync(binDir);
writeFileSync(
join(binDir, "gtimeout"),
`#!/bin/bash
set -euo pipefail
if [[ "$1" = "--kill-after=1s" ]]; then
exit 0
fi
printf 'gtimeout:%s %s|%s\\n' "$1" "$2" "\${*:3}" >>"$TMPDIR/timeout-seen"
shift 2
"$@"
`,
);
chmodSync(join(binDir, "gtimeout"), 0o755);
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
export PATH="$TMPDIR/bin"
export OPENCLAW_DOCKER_E2E_RUN_TIMEOUT=13s
docker() {
printf "%s\\n" "$*" >>"$TMPDIR/docker-seen"
}
export -f docker
source "$ROOT_DIR/scripts/lib/docker-e2e-container.sh"
docker_e2e_docker_run_cmd run demo
[[ "$(<"$TMPDIR/timeout-seen")" = "gtimeout:--kill-after=30s 13s|docker run demo" ]]
[[ "$(<"$TMPDIR/docker-seen")" = "run demo" ]]
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("keeps package-backed Docker runs bounded without the shared timeout helper", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-package-timeout-required-"));
try {
mkdirSync(join(workDir, "bin"));
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
export PATH="$TMPDIR/bin"
export OPENCLAW_DOCKER_E2E_RUN_TIMEOUT=11s
dirname() {
/usr/bin/dirname "$@"
}
docker_e2e_docker_cmd() {
return 0
}
docker() {
printf "%s\\n" "$*" >"$TMPDIR/docker-seen"
}
export -f docker_e2e_docker_cmd docker
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
set +e
docker_e2e_docker_run_cmd run demo 2>"$TMPDIR/stderr"
status="$?"
set -e
stderr="$(<"$TMPDIR/stderr")"
[[ "$status" = "127" ]]
[[ "$stderr" = *"timeout command not found; cannot bound Docker run after 11s"* ]]
[[ ! -e "$TMPDIR/docker-seen" ]]
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("uses gtimeout for package-backed Docker runs without the shared timeout helper", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-package-gtimeout-"));
try {
const binDir = join(workDir, "bin");
mkdirSync(binDir);
writeFileSync(
join(binDir, "gtimeout"),
`#!/bin/bash
set -euo pipefail
if [[ "$1" = "--kill-after=1s" ]]; then
exit 0
fi
printf 'gtimeout:%s %s|%s\\n' "$1" "$2" "\${*:3}" >>"$TMPDIR/timeout-seen"
shift 2
"$@"
`,
);
chmodSync(join(binDir, "gtimeout"), 0o755);
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
export PATH="$TMPDIR/bin"
export OPENCLAW_DOCKER_E2E_RUN_TIMEOUT=15s
dirname() {
/usr/bin/dirname "$@"
}
docker_e2e_docker_cmd() {
return 0
}
docker() {
printf "%s\\n" "$*" >>"$TMPDIR/docker-seen"
}
export -f docker_e2e_docker_cmd docker
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
docker_e2e_docker_run_cmd run demo
[[ "$(<"$TMPDIR/timeout-seen")" = "gtimeout:--kill-after=30s 15s|docker run demo" ]]
[[ "$(<"$TMPDIR/docker-seen")" = "run demo" ]]
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("removes functional Docker build package inputs after the build", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-cleanup-"));