fix(ci): keep every curl of the boot check off environment proxies (#35)
Only the port pre-check of scripts/boot-from-copy.sh carried --noproxy. The wait loop and the two assertions still honoured http_proxy, so on a workstation with a proxy configured the script talked to the proxy instead of the server it had just started, and failed loudly. Export NO_PROXY='*' and no_proxy='*' at the top of the script, which covers every curl call, and drop the now-redundant --noproxy. The new test boots the script for real behind a fake proxy that answers everything and records what reaches it. It first proves the proxy is live (a bare curl with the same environment reaches it), then requires that nothing of the boot check reaches it and that the script passes. Red on the previous script (the wait loop and the first assertion went through the proxy), red again when the export is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
48c9a408f6
commit
fef9f69fc0
2 changed files with 63 additions and 11 deletions
|
|
@ -99,15 +99,12 @@ describe("what the boot check assumes of the Dockerfile", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Runs the script with a listener already on its port: without the pre-check,
|
// Spawns a command from the repository root with `env` over the test's own
|
||||||
// the wait loop could reach this neighbour and assert on a server it never
|
// environment; a key set to undefined is dropped (spawn ignores undefined
|
||||||
// started. Async spawn, so the listener keeps answering while the script runs.
|
// values). Async, so a listener in this process keeps answering meanwhile.
|
||||||
function runBootScript(env) {
|
function run(command, args, env) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn("sh", ["scripts/boot-from-copy.sh"], {
|
const child = spawn(command, args, { cwd: ROOT, env: { ...process.env, ...env } });
|
||||||
cwd: ROOT,
|
|
||||||
env: { ...process.env, ...env },
|
|
||||||
});
|
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
child.stderr.on("data", (d) => (stderr += d));
|
child.stderr.on("data", (d) => (stderr += d));
|
||||||
child.on("error", reject);
|
child.on("error", reject);
|
||||||
|
|
@ -115,7 +112,22 @@ function runBootScript(env) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runBootScript = (env) => run("sh", ["scripts/boot-from-copy.sh"], env);
|
||||||
|
|
||||||
|
// A port nothing listens on. The probe closes before the script starts: should
|
||||||
|
// something take the port in between, the script's own check refuses it and
|
||||||
|
// the test fails loudly.
|
||||||
|
async function freePort() {
|
||||||
|
const probe = http.createServer();
|
||||||
|
await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
||||||
|
const { port } = probe.address();
|
||||||
|
await new Promise((resolve) => probe.close(resolve));
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
describe("boot-from-copy.sh", () => {
|
describe("boot-from-copy.sh", () => {
|
||||||
|
// A listener already on the port: without the pre-check, the wait loop could
|
||||||
|
// reach this neighbour and assert on a server it never started.
|
||||||
test("refuses a port something else already holds", async () => {
|
test("refuses a port something else already holds", async () => {
|
||||||
const neighbour = http.createServer((req, res) => res.end("not the API"));
|
const neighbour = http.createServer((req, res) => res.end("not the API"));
|
||||||
await new Promise((resolve) => neighbour.listen(0, "127.0.0.1", resolve));
|
await new Promise((resolve) => neighbour.listen(0, "127.0.0.1", resolve));
|
||||||
|
|
@ -129,6 +141,39 @@ describe("boot-from-copy.sh", () => {
|
||||||
await new Promise((resolve) => neighbour.close(resolve));
|
await new Promise((resolve) => neighbour.close(resolve));
|
||||||
}
|
}
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
|
// Every curl of the script targets the loopback, and a proxy from the
|
||||||
|
// environment would answer in the server's place. Before #35 only the port
|
||||||
|
// check had --noproxy: the wait loop and the assertions reached the proxy.
|
||||||
|
test("never sends a request through a proxy from the environment", async () => {
|
||||||
|
// Answers everything, as a real proxy would, and records what reaches it.
|
||||||
|
const proxied = [];
|
||||||
|
const proxy = http.createServer((req, res) => {
|
||||||
|
proxied.push(`${req.method} ${req.url}`);
|
||||||
|
res.end("answered by the proxy");
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => proxy.listen(0, "127.0.0.1", resolve));
|
||||||
|
try {
|
||||||
|
const proxyUrl = `http://127.0.0.1:${proxy.address().port}`;
|
||||||
|
// ALL_PROXY as well, so that unsetting http_proxy alone would not pass.
|
||||||
|
// An inherited NO_PROXY would hide the defect, hence dropped.
|
||||||
|
const env = { http_proxy: proxyUrl, ALL_PROXY: proxyUrl, NO_PROXY: undefined, no_proxy: undefined };
|
||||||
|
const port = await freePort();
|
||||||
|
|
||||||
|
// The proxy is live: a bare curl with the same environment reaches it.
|
||||||
|
// Without this, "nothing reached the proxy" could pass for nothing.
|
||||||
|
await run("curl", ["-s", "-o", "/dev/null", "--max-time", "5", `http://127.0.0.1:${port}/`], env);
|
||||||
|
expect(proxied).toEqual([`GET http://127.0.0.1:${port}/`]);
|
||||||
|
proxied.length = 0;
|
||||||
|
|
||||||
|
const { code, stderr } = await runBootScript({ ...env, BOOT_PORT: String(port) });
|
||||||
|
|
||||||
|
expect(proxied).toEqual([]);
|
||||||
|
expect(code, stderr).toBe(0);
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve) => proxy.close(resolve));
|
||||||
|
}
|
||||||
|
}, 20000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The boot check only calls /health, so it proves the existence of the modules
|
// The boot check only calls /health, so it proves the existence of the modules
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
# Every curl below targets this machine's loopback, and a proxy from the
|
||||||
|
# environment (http_proxy, ALL_PROXY) would answer in its place: the port check
|
||||||
|
# would probe the proxy instead of the port, the wait loop and the assertions
|
||||||
|
# would talk to the proxy instead of the server started here. Both spellings,
|
||||||
|
# since curl reads the lowercase one first and an inherited no_proxy would win.
|
||||||
|
export NO_PROXY='*' no_proxy='*'
|
||||||
|
|
||||||
PORT="${BOOT_PORT:-3097}"
|
PORT="${BOOT_PORT:-3097}"
|
||||||
TOKEN="boot-from-copy-token"
|
TOKEN="boot-from-copy-token"
|
||||||
DOCKERFILE="Dockerfile"
|
DOCKERFILE="Dockerfile"
|
||||||
|
|
@ -65,11 +72,11 @@ CMD_SH=$(printf '%s' "$CMD_LINE" | node -e '
|
||||||
# check a server we never started. So refuse up front: the port counts as free
|
# check a server we never started. So refuse up front: the port counts as free
|
||||||
# only when curl gets "connection refused" (exit 7). Any other exit — an HTTP
|
# only when curl gets "connection refused" (exit 7). Any other exit — an HTTP
|
||||||
# answer (0), a listener that accepts and stalls (28), a non-HTTP service
|
# answer (0), a listener that accepts and stalls (28), a non-HTTP service
|
||||||
# (52, 56) — means it is taken. --noproxy keeps an http_proxy from answering
|
# (52, 56) — means it is taken. No proxy answers in the port's place: NO_PROXY
|
||||||
# in the port's place.
|
# is exported at the top.
|
||||||
command -v curl >/dev/null 2>&1 || fail "curl is required"
|
command -v curl >/dev/null 2>&1 || fail "curl is required"
|
||||||
RC=0
|
RC=0
|
||||||
curl -s -o /dev/null --noproxy '*' --max-time 2 "http://127.0.0.1:$PORT/" || RC=$?
|
curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/" || RC=$?
|
||||||
[ "$RC" = "7" ] || fail "port $PORT is already taken (curl exit $RC); set BOOT_PORT to a free port"
|
[ "$RC" = "7" ] || fail "port $PORT is already taken (curl exit $RC); set BOOT_PORT to a free port"
|
||||||
|
|
||||||
# --- Build the image's file set ---------------------------------------------
|
# --- Build the image's file set ---------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue