#!/usr/bin/env node // A stand-in for the `docker` CLI that deploy/update.sh drives: one Compose project (omb and caddy) whose state is a // JSON file, and whose volumes are real folders, so a copy or a restore moves real files. FAKE_DOCKER_DIR holds it all. import { spawnSync } from "node:child_process"; import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const dir = process.env.FAKE_DOCKER_DIR; const stateFile = join(dir, "state.json"); const state = JSON.parse(readFileSync(stateFile, "utf8")); const save = () => writeFileSync(stateFile, JSON.stringify(state, null, 1)); const args = process.argv.slice(2); const line = args.join(" "); appendFileSync(join(dir, "calls.log"), `${line}\n`); const out = (text) => process.stdout.write(`${text}\n`); const fail = (text, code = 1) => { process.stderr.write(`${text}\n`); save(); process.exit(code); }; const volume = (name) => join(dir, "volumes", name.replaceAll("/", "_")); /** An image by any name it has: its id, a tag, or a digest reference. */ const image = (ref) => state.images.find((candidate) => candidate.local && (candidate.id === ref || candidate.refs.includes(ref))); const envFile = () => (existsSync(join(process.cwd(), ".env")) ? readFileSync(join(process.cwd(), ".env"), "utf8") : ""); const pinned = () => process.env.OMB_IMAGE ?? /^OMB_IMAGE=(.*)$/m.exec(envFile())?.[1] ?? null; const composeImage = () => (state.composeReadsPin && pinned()) || state.defaultImage; /** `{step: "compose up", signal: "SIGKILL"}`: the update script is signalled as this call starts, once. `match`, a * regular expression, instead picks the call by any part of it. */ const trip = state.signalOn; if (trip && (trip.match ? new RegExp(trip.match).test(line) : line.startsWith(trip.step))) { delete state.signalOn; save(); process.kill(Number(readFileSync(join(process.cwd(), ".update", "lock", "pid"), "utf8")), trip.signal); if (trip.signal === "SIGKILL") process.exit(137); } function composeUp() { const wanted = image(composeImage()); if (!wanted) fail(`pull access denied for ${composeImage()}`); if (state.failUp === wanted.id) fail("Error response from daemon: driver failed programming external connectivity"); const crashes = wanted.behaviour === "crashes"; state.containers.omb = { id: `omb-${++state.serial}`, image: wanted.id, status: crashes ? "restarting" : "running", restarts: crashes ? 5 : 0 }; state.containers.caddy = { id: `caddy-${state.serial}`, status: wanted.behaviour === "no-caddy" ? "exited" : "running" }; // A server that starts writes to its data, as a new version's migrations do. if (!crashes) appendFileSync(join(volume(state.dataVolume), "started-by.txt"), `${wanted.version}\n`); } function compose(rest) { const [verb, ...more] = rest; const omb = state.containers.omb; if (verb === "version") return out("Docker Compose version v2.40.0"); if (verb === "config") return out(`services:\n omb:\n image: ${composeImage()}`); if (verb === "ps" && more.includes("-aq")) return omb ? out(omb.id) : undefined; if (verb === "ps") return Object.entries(state.containers).filter(([, container]) => container?.status === "running").forEach(([name]) => out(name)); if (verb === "stop") { if (state.failStop) fail("Error response from daemon: cannot stop container: permission denied"); for (const container of Object.values(state.containers)) if (container) Object.assign(container, { status: "exited" }); return; } if (verb === "up") return composeUp(); if (verb === "logs") return out(image(omb?.image)?.lastWords ?? ""); if (verb === "exec") { // As the real one does: `docker compose exec` forwards its standard input to the container, so it takes whatever is // waiting there, an answer meant for update.sh's question included. try { readFileSync(0); } catch { /* no standard input */ } const serving = image(omb?.image); if (omb?.status !== "running" || !serving) fail("service omb is not running"); // `doctor: false`: a hardened version, where doctor without a session is refused whatever its engines. if (more.includes("openmausbot")) return serving.engine && serving.doctor !== false ? out("ready") : fail("no engine is available", 5); if (serving.behaviour === "silent") fail("", 7); // A version with the count (`counts`) answers how many engines can start a bot; an older one has no such route. if (line.includes("/api/health/engines")) return serving.counts ? out(JSON.stringify({ app: "openmausbot", engines: serving.engine ? 1 : 0 })) : fail("", 22); if (line.includes("/api/health")) return out('{"app":"openmausbot"}'); if (line.includes("/environment")) return out(JSON.stringify({ environmentId: "e", version: serving.serves ?? serving.version })); } fail(`fake docker: unknown compose command: ${line}`, 64); } function inspect(format, target) { const omb = state.containers.omb; if (!omb || omb.id !== target) fail(`No such object: ${target}`); if (format === "{{.Image}}") return out(omb.image); if (format === "{{.State.Running}}") return out(String(omb.status === "running")); if (format === "{{.State.Status}} {{.RestartCount}} {{.Image}}") return out(`${omb.status} ${omb.restarts} ${omb.image}`); if (format.includes(".Mounts")) return out(state.dataVolume); if (format.includes("com.docker.compose.project")) return out("deploy"); fail(`fake docker: unknown inspect format: ${format}`, 64); } function imageInspect(format, ref) { const found = image(ref); if (!found) fail(`No such image: ${ref}`); if (format === "{{.Id}}") return out(found.id); if (format.includes(".RepoDigests")) return found.refs.filter((name) => name.includes("@sha256:")).forEach((name) => out(name)); // `label`: what the image says it is, when that is not its version (a build of main says the branch). if (format.includes("image.version")) return out(found.label ?? (found.version ? `v${found.version}` : "")); fail(`fake docker: unknown image format: ${format}`, 64); } /** `run … -v a:/from:ro -v b:/to --entrypoint `: the command really runs, on the volumes' folders. */ function run(rest) { const mounts = {}; rest.forEach((arg, at) => { if (arg !== "-v") return; const [source, target] = rest[at + 1].split(":"); mounts[target] = volume(source); }); const entry = rest[rest.indexOf("--entrypoint") + 1]; const tail = rest.slice(rest.indexOf("--entrypoint") + 3); const script = entry === "sh" ? tail[1] : ""; if (state.failCopy && entry === "cp") fail("cp: error writing '/to/messages.db': No space left on device"); // A restore that fails while it copies into its own folder, before the data is touched (a full disk, a killed helper). if (state.failRestore && script.includes(".omb-restore")) fail("cp: cannot create regular file '/to/.omb-restore/messages.db': No space left on device"); if (Object.values(mounts).some((folder) => !folder.startsWith(join(dir, "volumes")))) fail("fake docker: a mount outside the fixture", 64); const inFolders = (text) => text.replace(/\/(from|to)(?=[/\s.]|$)/g, (_, name) => mounts[`/${name}`] ?? join(dir, "volumes", "unmounted")); // Every /from and /to must become a fixture folder: a script that names one any other way would reach the real disk. if (tail.map(inFolders).some((arg) => /(^|[^\w.-])\/(from|to)\b/.test(arg))) fail("fake docker: a container path the fixture cannot map", 64); if (script.startsWith("du -sk")) return out(`${state.dataKb ?? 1024}\n${state.freeKb ?? 50_000_000}`); const done = spawnSync(entry, tail.map(inFolders), { stdio: "inherit" }); if (done.status !== 0) fail(`helper exited ${done.status}`, done.status ?? 1); // `corruptCopy`: the copy lands, and then a file in it is not what was copied (a bad disk, a copy cut short). if (state.corruptCopy && entry === "cp") { delete state.corruptCopy; appendFileSync(join(mounts["/to"], "started-by.txt"), "garbled\n"); } } function main() { const [verb, ...rest] = args; if (verb === "compose") return compose(rest); if (verb === "inspect") return inspect(rest[1], rest[2]); if (verb === "image" && rest[0] === "inspect") return imageInspect(rest[2], rest[3]); if (verb === "pull") { const ref = rest.at(-1); const found = state.images.find((candidate) => candidate.refs.includes(ref)); if (!found || state.failPull) fail(`Error response from daemon: manifest for ${ref} not found`); found.local = true; return; } if (verb === "tag") return void image(rest[0])?.refs.push(rest[1]); if (verb === "run") return run(rest); if (verb === "volume") { const name = rest.at(-1); state.volumeLabels ??= {}; if (rest[0] === "rm") { delete state.volumeLabels[name]; return rmSync(volume(name), { recursive: true, force: true }); } if (rest[0] === "create") { state.volumeLabels[name] = rest.flatMap((arg, at) => (rest[at - 1] === "--label" ? [arg] : [])); return mkdirSync(volume(name), { recursive: true }); } if (rest[0] === "inspect") return existsSync(volume(name)) ? out(name) : fail(`no such volume: ${name}`); if (rest[0] === "ls") { const wanted = rest.flatMap((arg, at) => (rest[at - 1] === "--filter" && arg.startsWith("label=") ? [arg.slice("label=".length)] : [])); return Object.entries(state.volumeLabels).filter(([, labels]) => wanted.every((label) => labels.includes(label))).forEach(([found]) => out(found)); } } fail(`fake docker: unknown command: ${line}`, 64); } main(); save();