Files
jschoubben bd5b349a0d route-adapter: provide route by writing into the predecessor's proxy (hq ADR 0104)
A node being adopted cannot take a web module: every module reachable by
name requires route, the mesh's only provider of it binds the two public
ports, and the predecessor's proxy holds them and serves every public
name there. Stopping the predecessor to break the circle darkens every
name at once, with every certificate to re-obtain in the same window.

So this answers the same provision without binding anything. It provides
route and receives the same contributions file, and writes each
contribution as one route file where the predecessor's file provider
reads, naming the predecessor's own certificate resolver so no
certificate is asked for. It removes a file it wrote when its
contribution goes and never touches a file it did not write — the name
and a marker inside both have to say it is the mesh's.

A step, not a daemon: run-once, re-run by restart-on over the received
file and the settings. The predecessor's dynamic directory is a node
setting, because it is a fact about one machine.

Migration scaffolding with a stated end: assigned only on an adopted
node, deleted when the predecessor's proxy retires.
2026-09-23 00:11:21 +02:00

51 lines
2.4 KiB
TypeScript

// route-adapter's one pass — the module's own code (novox/hq ADR 0039), run as a step (ADR 0052)
// and run *again* whenever what it reads changes.
//
// **A step, not a loop.** Everything this module does is a function of two files the mesh writes:
// the contributions at `receives.route`, and its settings. The manifest's container is `run-once`
// and names both under `restart-on`, so the host runs it once and runs it again the moment either
// changes — which is the mechanism already in the catalogue for "a fact arrived, do it again"
// (novox/hq ADR 0099). A watcher of its own would be a second way to notice the same event, and it
// would keep running after the module was unassigned.
//
// It connects to no broker: this is an offline file operation on the machine it runs on, and
// `mesh-tools run` gives it exactly that.
import { readFile } from "node:fs/promises";
import { reconcile, routesFrom, settingsFrom } from "./adapter.js";
const receives = process.env.MESH_RECEIVES ?? "/var/lib/route-adapter/routes/mesh.json";
const configFile = process.env.MESH_ROUTE_ADAPTER_CONFIG ?? "/run/config/config.json";
// The settings the node laid over the module's defaults. Absent is not an error — the mesh always
// writes the file, and a node that overrode nothing leaves it holding the defaults.
const settings = settingsFrom(await readJSON(configFile));
// The contributions. Absent is also not an error here: the host writes this file before it starts
// the container, and the empty case — nothing contributed — means every route this module wrote is
// to be taken away, which is a real instruction and not a reason to stop.
const { routes, skipped } = routesFrom(await readJSON(receives), settings.machine);
for (const why of skipped) {
console.warn(`[route-adapter] ${why}`);
}
const pass = await reconcile(routes, settings);
for (const why of pass.skipped) {
console.warn(`[route-adapter] ${why}`);
}
console.log(
`[route-adapter] ${routes.length} route(s) contributed; ` +
`wrote ${pass.written.length} and removed ${pass.removed.length} in ${settings.dynamic}` +
(pass.written.length > 0 ? `: ${pass.written.join(", ")}` : "") +
(pass.removed.length > 0 ? `; gone: ${pass.removed.join(", ")}` : ""),
);
async function readJSON(path: string): Promise<unknown> {
const raw = await readFile(path, "utf8").catch(() => undefined);
if (raw === undefined) {
return undefined;
}
return JSON.parse(raw) as unknown;
}