mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(docker): bound smoke installer curl with connect timeout (#109698)
* fix(docker): bound smoke installer curl with connect timeout Add --connect-timeout 30 and --max-time 300 to the direct installer one-liner curl in the smoke test runner. The run_installer_for_package_spec function is already bounded by timeout(1), but this direct invocation had no curl-level or wrapper-level timeout. Co-Authored-By: Claude <noreply@anthropic.com> * fix(docker): wrap complete installer pipeline in timeout, not just curl Replace the stand-alone curl timeout flags on the direct installer one-liner with a timeout(1) wrapper around the entire curl|bash pipeline, matching the run_installer_for_package_spec pattern. This bounds the complete pipeline (download + install) rather than just the network transfer. The curl-level --connect-timeout 30 and --max-time 300 are retained as defence-in-depth inside the timeout wrapper. Co-Authored-By: Claude <noreply@anthropic.com> * refactor: centralize installer smoke pipeline bounds Co-authored-by: LZY3538 <liu.zhenye@xydigit.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -252,13 +252,16 @@ resolve_update_baseline_version() {
|
||||
UPDATE_BASELINE_VERSION="$resolved_version"
|
||||
}
|
||||
|
||||
run_installer_for_package_spec() {
|
||||
run_installer_pipeline() {
|
||||
local install_url="$1"
|
||||
local package_spec="$2"
|
||||
shift
|
||||
|
||||
# Keep both pipeline processes under one timeout, and preserve download failures
|
||||
# even when the installer shell consumes a partial response and exits cleanly.
|
||||
timeout --kill-after=30s "${INSTALL_COMMAND_TIMEOUT}s" \
|
||||
bash -c "curl -fsSL \"\$1\" | bash -s -- --install-method npm --version \"\$2\" --no-prompt --no-onboard" \
|
||||
_ "$install_url" "$package_spec"
|
||||
bash -o pipefail -c \
|
||||
'curl -fsSL --connect-timeout 30 --max-time 300 -- "$1" | bash -s -- "${@:2}"' \
|
||||
_ "$install_url" "$@"
|
||||
}
|
||||
|
||||
run_install_smoke() {
|
||||
@@ -267,7 +270,12 @@ run_install_smoke() {
|
||||
echo "==> Run official installer one-liner for latest release tarball"
|
||||
OPENCLAW_NO_ONBOARD=1 OPENCLAW_NO_PROMPT=1 \
|
||||
run_with_heartbeat "installer latest release tarball" \
|
||||
run_installer_for_package_spec "$INSTALL_URL" "$FRESH_TAG_URL"
|
||||
run_installer_pipeline \
|
||||
"$INSTALL_URL" \
|
||||
--install-method npm \
|
||||
--version "$FRESH_TAG_URL" \
|
||||
--no-prompt \
|
||||
--no-onboard
|
||||
print_install_audit "fresh install"
|
||||
|
||||
echo "==> Verify installed version"
|
||||
@@ -335,7 +343,7 @@ NODE
|
||||
fi
|
||||
|
||||
echo "==> Run official installer one-liner"
|
||||
curl -fsSL "$INSTALL_URL" | bash -s -- --no-prompt
|
||||
run_installer_pipeline "$INSTALL_URL" --no-prompt
|
||||
|
||||
echo "==> Verify installed version"
|
||||
if [[ -n "${OPENCLAW_INSTALL_LATEST_OUT:-}" ]]; then
|
||||
@@ -594,13 +602,16 @@ run_freshness_smoke() {
|
||||
fi
|
||||
|
||||
echo "==> Run installer with same npm freshness policy"
|
||||
env \
|
||||
HOME="$policy_home" \
|
||||
NPM_CONFIG_USERCONFIG="${policy_home}/.npmrc" \
|
||||
OPENCLAW_NO_ONBOARD=1 \
|
||||
OPENCLAW_NO_PROMPT=1 \
|
||||
bash -c 'curl -fsSL "$1" | bash -s -- --install-method npm --version "$2" --no-prompt --no-onboard' \
|
||||
_ "$INSTALL_URL" "$FRESHNESS_VERSION"
|
||||
HOME="$policy_home" \
|
||||
NPM_CONFIG_USERCONFIG="${policy_home}/.npmrc" \
|
||||
OPENCLAW_NO_ONBOARD=1 \
|
||||
OPENCLAW_NO_PROMPT=1 \
|
||||
run_installer_pipeline \
|
||||
"$INSTALL_URL" \
|
||||
--install-method npm \
|
||||
--version "$FRESHNESS_VERSION" \
|
||||
--no-prompt \
|
||||
--no-onboard
|
||||
|
||||
echo "==> Verify installed version"
|
||||
print_install_audit "freshness install"
|
||||
|
||||
@@ -284,6 +284,108 @@ function validateInstallSmokeUpdateJson(doctorStep?: Record<string, unknown>) {
|
||||
});
|
||||
}
|
||||
|
||||
function extractInstallSmokeInstallerPipeline(): string {
|
||||
const script = readFileSync(SMOKE_RUNNER_PATH, "utf8");
|
||||
const match = script.match(
|
||||
/(run_installer_pipeline\(\) \{[\s\S]*?\n\})\n\nrun_install_smoke/u,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error("install smoke installer pipeline helper was not found");
|
||||
}
|
||||
return expectDefined(match[1], "install smoke installer pipeline helper capture");
|
||||
}
|
||||
|
||||
function readNulSeparatedArgs(filePath: string): string[] {
|
||||
return readFileSync(filePath, "utf8").split("\0").filter(Boolean);
|
||||
}
|
||||
|
||||
function runInstallSmokeInstallerPipelineFixture(params: {
|
||||
curlExitCode?: number;
|
||||
installerArgs: string[];
|
||||
}) {
|
||||
const root = tempDirs.make("openclaw-install-smoke-pipeline-");
|
||||
const binDir = join(root, "bin");
|
||||
const curlArgsPath = join(root, "curl-args.txt");
|
||||
const installerArgsPath = join(root, "installer-args.txt");
|
||||
const installerMarkerPath = join(root, "installer-ran");
|
||||
const installerSourcePath = join(root, "installer.sh");
|
||||
const timeoutArgsPath = join(root, "timeout-args.txt");
|
||||
const installUrl = "https://installer.example.test/install.sh?channel=beta&trace=1";
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(binDir, "timeout"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\0' "$@" >"$TIMEOUT_ARGS_PATH"`,
|
||||
"shift 2",
|
||||
'exec "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
writeFileSync(
|
||||
join(binDir, "curl"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\0' "$@" >"$CURL_ARGS_PATH"`,
|
||||
'cat "$FAKE_INSTALLER_SOURCE"',
|
||||
'exit "$FAKE_CURL_EXIT"',
|
||||
"",
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
writeFileSync(
|
||||
installerSourcePath,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\0' "$@" >"$INSTALLER_ARGS_PATH"`,
|
||||
'touch "$INSTALLER_MARKER_PATH"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
[
|
||||
"--noprofile",
|
||||
"--norc",
|
||||
"-c",
|
||||
`set -euo pipefail
|
||||
${extractInstallSmokeInstallerPipeline()}
|
||||
run_installer_pipeline "$INSTALL_URL" "$@"`,
|
||||
"_",
|
||||
...params.installerArgs,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CURL_ARGS_PATH: curlArgsPath,
|
||||
FAKE_CURL_EXIT: String(params.curlExitCode ?? 0),
|
||||
FAKE_INSTALLER_SOURCE: installerSourcePath,
|
||||
INSTALLER_ARGS_PATH: installerArgsPath,
|
||||
INSTALLER_MARKER_PATH: installerMarkerPath,
|
||||
INSTALL_COMMAND_TIMEOUT: "17",
|
||||
INSTALL_URL: installUrl,
|
||||
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
TIMEOUT_ARGS_PATH: timeoutArgsPath,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
curlArgsPath,
|
||||
installUrl,
|
||||
installerArgsPath,
|
||||
installerMarkerPath,
|
||||
result,
|
||||
timeoutArgsPath,
|
||||
};
|
||||
}
|
||||
|
||||
function expectInstallDockerfileContract(
|
||||
dockerfilePath: string,
|
||||
runnerPath: string,
|
||||
@@ -1024,11 +1126,27 @@ printf 'status=%s\\n' "$status"
|
||||
|
||||
expect(wrapper).toContain('-v "$ROOT_DIR/scripts/install.sh:/tmp/openclaw-install.sh:ro"');
|
||||
expect(runner).toContain("Run official installer one-liner for latest release tarball");
|
||||
expect(runner).toContain("run_installer_for_package_spec");
|
||||
expect(runner).toContain('bash -c "curl -fsSL \\"\\$1\\" | bash -s --');
|
||||
expect(runner).toContain("run_installer_pipeline");
|
||||
expect(runner).toContain('--version "$FRESH_TAG_URL"');
|
||||
expect(runner).not.toContain('npm_install_global "install latest release tarball"');
|
||||
});
|
||||
|
||||
it("uses one bounded installer pipeline for candidate, default, and freshness smoke", () => {
|
||||
const runner = readFileSync(SMOKE_RUNNER_PATH, "utf8");
|
||||
|
||||
expect(runner.match(/^\s*run_installer_pipeline\b/gmu)).toHaveLength(4);
|
||||
expect(runner).toContain("bash -o pipefail -c");
|
||||
expect(
|
||||
runner.match(/curl -fsSL --connect-timeout 30 --max-time 300 --/gu),
|
||||
).toHaveLength(1);
|
||||
expect(runner).toContain('run_installer_pipeline "$INSTALL_URL" --no-prompt');
|
||||
expect(runner).toContain('--version "$FRESH_TAG_URL"');
|
||||
expect(runner).toContain('--version "$FRESHNESS_VERSION"');
|
||||
expect(runner).toMatch(
|
||||
/HOME="\$policy_home" \\\n\s*NPM_CONFIG_USERCONFIG="\$\{policy_home\}\/\.npmrc" \\\n\s*OPENCLAW_NO_ONBOARD=1 \\\n\s*OPENCLAW_NO_PROMPT=1 \\\n\s*run_installer_pipeline/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses public npm latest as the non-root installer expectation", () => {
|
||||
const wrapper = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -1200,6 +1318,60 @@ describe("install-sh E2E runner", () => {
|
||||
});
|
||||
|
||||
describe("install-sh smoke runner", () => {
|
||||
it("passes the URL and installer arguments through the timed pipeline unchanged", () => {
|
||||
const installerArgs = [
|
||||
"--install-method",
|
||||
"npm",
|
||||
"--version",
|
||||
"https://packages.example.test/openclaw.tgz?x=1&y=2",
|
||||
"--no-prompt",
|
||||
];
|
||||
const fixture = runInstallSmokeInstallerPipelineFixture({ installerArgs });
|
||||
|
||||
expect(fixture.result.status, fixture.result.stderr).toBe(0);
|
||||
expect(readNulSeparatedArgs(fixture.timeoutArgsPath)).toEqual([
|
||||
"--kill-after=30s",
|
||||
"17s",
|
||||
"bash",
|
||||
"-o",
|
||||
"pipefail",
|
||||
"-c",
|
||||
'curl -fsSL --connect-timeout 30 --max-time 300 -- "$1" | bash -s -- "${@:2}"',
|
||||
"_",
|
||||
fixture.installUrl,
|
||||
...installerArgs,
|
||||
]);
|
||||
expect(readNulSeparatedArgs(fixture.curlArgsPath)).toEqual([
|
||||
"-fsSL",
|
||||
"--connect-timeout",
|
||||
"30",
|
||||
"--max-time",
|
||||
"300",
|
||||
"--",
|
||||
fixture.installUrl,
|
||||
]);
|
||||
expect(readNulSeparatedArgs(fixture.installerArgsPath)).toEqual(installerArgs);
|
||||
expect(existsSync(fixture.installerMarkerPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("propagates curl exit 28 even when the piped installer exits successfully", () => {
|
||||
const fixture = runInstallSmokeInstallerPipelineFixture({
|
||||
curlExitCode: 28,
|
||||
installerArgs: ["--no-prompt"],
|
||||
});
|
||||
|
||||
expect(fixture.result.status, fixture.result.stderr).toBe(28);
|
||||
expect(existsSync(fixture.installerMarkerPath)).toBe(true);
|
||||
expect(readNulSeparatedArgs(fixture.timeoutArgsPath).slice(0, 6)).toEqual([
|
||||
"--kill-after=30s",
|
||||
"17s",
|
||||
"bash",
|
||||
"-o",
|
||||
"pipefail",
|
||||
"-c",
|
||||
]);
|
||||
});
|
||||
|
||||
it("wraps long npm/update operations with heartbeat and install-size audits", () => {
|
||||
const script = readFileSync(SMOKE_RUNNER_PATH, "utf8");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user