ci: keep the boot check off proxies, catch continue-on-error in any literal spelling (#35) #42
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