// @ts-check /** * `wget` in the image (`/usr/local/bin/wget` runs this file): enough of wget * for a container health check, and nothing more. * * The runtime base has no wget, but health checks written outside the image * call it: compose files copied from this repository up to v1.46.6 run * `wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health`, * and the TrueNAS catalog app renders `wget --quiet --spider `. Such a * check replaces the image's own HEALTHCHECK, so without a `wget` the * container would never turn healthy. The image's HEALTHCHECK and the * bundled compose file use a `node -e` fetch instead; this file exists only * for those external callers. * * Understood: the URL, `--spider`, `-q`/`--quiet`, `-nv`/`--no-verbose`, * `-t N`/`--tries=N`, `-T N`/`--timeout=N`, `-O -` and `-O /dev/null` * (also `-O-`, `--output-document=…`), `-S`/`--server-response`. Any other * flag is ignored. It is not a download tool: it writes a body only to * standard output (`-O -`), refuses any other `-O` target before sending a * request, never follows a redirect, and speaks plain http/https only. * * Exit 0 on a 2xx or 3xx answer, 1 on any other status, a refused or * failed connection, or the timeout, and 2 on a command line it will not * run. Without `-T` a request gives up after 30 s; Docker's own health * check timeout usually ends it sooner. */ import { pathToFileURL } from "node:url"; const DEFAULT_TIMEOUT_SECONDS = 30; /** Flags whose value may follow as the next argument. */ const VALUE_FLAGS = new Set(["-O", "-T", "-t", "-o", "-U", "-P", "--header"]); /** * @param {string[]} argv * @returns {{ url: string | null; spider: boolean; quiet: boolean; * verbose: boolean; tries: number; timeout: number; output: string | null; * serverResponse: boolean }} */ function parseArgs(argv) { const opts = { url: /** @type {string | null} */ (null), spider: false, quiet: false, verbose: true, tries: 1, timeout: DEFAULT_TIMEOUT_SECONDS, output: /** @type {string | null} */ (null), serverResponse: false, }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i] ?? ""; /** @param {string} name */ const inline = (name) => arg.startsWith(`${name}=`) ? arg.slice(name.length + 1) : null; /** The value of a short flag, attached (`-T4`) or as the next argument. */ const shortValue = () => { if (arg.length > 2) return arg.slice(2).replace(/^=/, ""); i += 1; return argv[i] ?? ""; }; if (/^https?:\/\//i.test(arg)) { opts.url ??= arg; } else if (/^[a-z0-9.[\]:-]+(:\d+)?\//i.test(arg)) { // busybox wget takes `127.0.0.1:3000/api/health` without a scheme. opts.url ??= `http://${arg}`; } else if (arg === "--spider") { opts.spider = true; } else if (arg === "-q" || arg === "--quiet") { opts.quiet = true; } else if (arg === "-nv" || arg === "--no-verbose") { opts.verbose = false; } else if (arg === "-S" || arg === "--server-response") { opts.serverResponse = true; } else if (arg.startsWith("--tries=")) { opts.tries = count(inline("--tries"), 1); } else if (arg.startsWith("-t")) { opts.tries = count(shortValue(), 1); } else if (arg.startsWith("--timeout=")) { opts.timeout = count(inline("--timeout"), DEFAULT_TIMEOUT_SECONDS); } else if (arg.startsWith("-T")) { opts.timeout = count(shortValue(), DEFAULT_TIMEOUT_SECONDS); } else if (arg.startsWith("--output-document=")) { opts.output = inline("--output-document"); } else if (arg.startsWith("-O")) { opts.output = shortValue(); } else if (VALUE_FLAGS.has(arg)) { // A flag this shim ignores, with its value as the next argument. i += 1; } else if (/^-q[a-zA-Z]*O/.test(arg)) { // `-qO-` / `-qO /dev/null`: quiet plus an output target, bundled. opts.quiet = true; const rest = arg.slice(arg.indexOf("O") + 1); opts.output = rest || (argv[++i] ?? ""); } // Anything else is ignored. } // wget treats 0 tries as "retry forever"; a health check must not. if (opts.tries < 1) opts.tries = 1; return opts; } /** * @param {string | null} value * @param {number} fallback */ function count(value, fallback) { const n = Number(value); return Number.isFinite(n) && n >= 0 ? n : fallback; } /** @param {string} message @param {boolean} quiet */ function say(message, quiet) { if (!quiet) process.stderr.write(`wget: ${message}\n`); } /** * @param {string[]} argv * @returns {Promise} the exit code */ async function run(argv) { const opts = parseArgs(argv); if (!opts.url) { process.stderr.write("wget: missing URL (health check shim)\n"); return 2; } const toStdout = opts.output === "-"; if (opts.output !== null && !toStdout && opts.output !== "/dev/null") { process.stderr.write( `wget: refusing to write ${opts.output}: this image's wget is a health check shim (-O - and -O /dev/null only)\n`, ); return 2; } let lastError = ""; for (let attempt = 1; attempt <= opts.tries; attempt += 1) { try { const response = await fetch(opts.url, { redirect: "manual", signal: AbortSignal.timeout(Math.max(opts.timeout, 0.001) * 1000), }); if (opts.serverResponse) { process.stderr.write( ` HTTP/1.1 ${response.status} ${response.statusText}\n`, ); for (const [name, value] of response.headers) { process.stderr.write(` ${name}: ${value}\n`); } } if (toStdout && !opts.spider && response.body) { for await (const chunk of response.body) process.stdout.write(chunk); } else { await response.body?.cancel(); } const ok = response.status >= 200 && response.status < 400; if (ok) { if (opts.verbose) say(`${opts.url}: ${response.status}`, opts.quiet); return 0; } say(`server returned error: HTTP ${response.status}`, opts.quiet); return 1; } catch (err) { lastError = err instanceof Error && err.name === "TimeoutError" ? "download timed out" : err instanceof Error ? err.cause instanceof Error ? err.cause.message : err.message : String(err); } } say(`${opts.url}: ${lastError}`, opts.quiet); return 1; } if ( process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href ) { run(process.argv.slice(2)).then( (code) => process.exit(code), () => process.exit(1), ); }