The bed bootstraps through the installer, not around it
ADR 0067's own acceptance check said the lab must raise its anchor by running the program a bare machine runs. It did not: whole-mesh-full applied the substrate bundle by hand and then looped enrolment over all four machines as one continuous operation. That gets the order right by accident and models the wrong shape — and an install procedure that exists only as a test fixture is exercised by whoever writes tests and never by whoever installs, which is why every bootstrap fault this year was found late. Two acts now, and the first gates the second. GENESIS is novox running mesh-bootstrap: the installer is built from source before the raise (make bootstrap, carrying the control-plane image built in the same run), placed beside the host binary, given the two manifests it reads, and run. The bed then asserts a WORKING MESH OF ONE — the control plane answers, the registry replies on /v2/, the container called mesh-control is running from a registry-pinned digest rather than an image id, the registry agrees it serves it, temp-mesh-control is gone, and the mesh has heard from its node. The image-id check is ADR 0067's "the pivot completed" verbatim: if it is still an id, nothing was published and this mesh can never roll out its own upgrades. JOINING is ace, shanks and g14: host binary, token, enrol, run. novox is NOT enrolled again — the installer already did it, and a second identity is one the mesh does not know. If genesis stops, the bed prints which of the installer's ten steps it stopped at and goes no further. A second machine joining a mesh that is not ready is a different failure, and running it would bury this one underneath it. The anchor is no longer handed mesh-control:development. Its absence is the point: the installer carries that image inside itself, and handing it over as well would make the load say "already held" and leave the carrying untested — the same class of fiction the lab's own registry used to hide. A unit test asserts the scenario keeps it out. The registry is reached at 127.0.0.1:5000, which is a finding rather than a shortcut: a runtime refuses a plain-HTTP registry at any address but a loopback one, so the digest the control-plane module is pinned to is one only the anchor can pull. Enough here, because only the anchor runs a control plane. Written down in the bed. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -61,11 +61,45 @@ export async function exec(
|
||||
*/
|
||||
timeoutMs = 120_000,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
const name = await instanceNameOf(instanceId, machine);
|
||||
return incus(["exec", name, "--", ...command], timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the hypervisor calls one of a scenario's machines.
|
||||
*
|
||||
* Asked of the daemon by the metadata each instance carries, and only derived from the naming
|
||||
* rule when it answers nothing — the same order `exec` has always used. It is exported because
|
||||
* the placement stage takes this name rather than the pair, and a caller that wants to put
|
||||
* something on one machine after the raise (the installer, a catalogue checkout) would otherwise
|
||||
* have to reimplement the lookup and get the fallback wrong.
|
||||
*/
|
||||
export async function instanceNameOf(instanceId: string, machine: string): Promise<string> {
|
||||
const found = (await taggedInstances()).find(
|
||||
(i) => i.instanceId === instanceId && i.machine === machine,
|
||||
);
|
||||
const name = found?.name ?? machineName(instanceId, machine);
|
||||
return incus(["exec", name, "--", ...command], timeoutMs);
|
||||
return found?.name ?? machineName(instanceId, machine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a file from this workstation inside a machine.
|
||||
*
|
||||
* The long default timeout is not generosity: what goes through here is an installer carrying a
|
||||
* container image, which is tens of megabytes, and a push that is merely slow must not look like
|
||||
* a push that is stuck.
|
||||
*/
|
||||
export async function push(
|
||||
instanceId: string,
|
||||
machine: string,
|
||||
local: string,
|
||||
remote: string,
|
||||
mode?: string,
|
||||
timeoutMs = 900_000,
|
||||
): Promise<void> {
|
||||
const name = await instanceNameOf(instanceId, machine);
|
||||
const args = ["file", "push", local, `${name}${remote}`];
|
||||
if (mode) args.push("--mode", mode);
|
||||
await incus(args, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,17 @@ export function isPlaceable(artifact: string): boolean {
|
||||
/** Where the host binary lives on a machine once placed. */
|
||||
export const HOST_PATH = "/usr/local/bin/mesh-host";
|
||||
|
||||
/**
|
||||
* Where the installer lives on a machine once placed.
|
||||
*
|
||||
* **Beside the host, because it is the same tier and the same delivery** (novox/hq ADR 0067):
|
||||
* bootstrapping is done by hand and it changes a machine, which is what tier 0 is — but `mesh-host`
|
||||
* says of itself that it connects to nothing and listens on nothing, and an installer that loads
|
||||
* images and interrogates a control plane cannot be folded into it without making that sentence
|
||||
* false. Two programs, one shelf.
|
||||
*/
|
||||
export const BOOTSTRAP_PATH = "/usr/local/bin/mesh-bootstrap";
|
||||
|
||||
export interface Placement {
|
||||
machine: string;
|
||||
artifacts: string[];
|
||||
@@ -78,6 +89,14 @@ export function hostBinaryPath(): string | null {
|
||||
return process.env["MESH_LAB_HOST_BINARY"] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The installer to place, from the environment. Same rule as the host binary: an explicit path,
|
||||
* and nothing is guessed.
|
||||
*/
|
||||
export function bootstrapBinaryPath(): string | null {
|
||||
return process.env["MESH_LAB_BOOTSTRAP_BINARY"] ?? null;
|
||||
}
|
||||
|
||||
export class PlacementError extends Error {
|
||||
readonly machine: string;
|
||||
|
||||
@@ -146,6 +165,41 @@ export async function placeHost(
|
||||
return { machine, profile, version };
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the installer on a machine and ask it what it is.
|
||||
*
|
||||
* The same shape as {@link placeHost} and for the same reason: the version is read back from the
|
||||
* running binary, because a file arriving is not a program working (novox/hq ADR 0018). The
|
||||
* installer is the larger of the two by an order of magnitude — it carries a saved container image
|
||||
* — so a copy that half-arrived is a real possibility rather than a theoretical one.
|
||||
*
|
||||
* It is placed on ONE machine, not all of them. Only the anchor is bootstrapped; every other
|
||||
* machine joins a mesh that already exists, with the host binary and a token and nothing else.
|
||||
*/
|
||||
export async function placeBootstrap(
|
||||
instanceName: string,
|
||||
machine: string,
|
||||
binary: string,
|
||||
log: (message: string) => void = () => {},
|
||||
): Promise<string> {
|
||||
await incus(["file", "push", binary, `${instanceName}${BOOTSTRAP_PATH}`, "--mode", "0755"],
|
||||
900_000);
|
||||
|
||||
const version = (await incusOk(
|
||||
["exec", instanceName, "--", BOOTSTRAP_PATH, "version"], 60_000,
|
||||
))?.trim();
|
||||
if (!version) {
|
||||
throw new PlacementError(
|
||||
machine,
|
||||
`the installer was copied to ${machine} and does not run there. It carries a saved ` +
|
||||
`container image and is twenty megabytes or so, which is exactly the size at which a ` +
|
||||
`truncated copy stops being theoretical.`,
|
||||
);
|
||||
}
|
||||
log(` placed the installer on ${machine} (${version})`);
|
||||
return version;
|
||||
}
|
||||
|
||||
/** Place everything a scenario declares. */
|
||||
export async function applyPlacements(
|
||||
scenario: Scenario,
|
||||
|
||||
+33
-1
@@ -2,7 +2,8 @@
|
||||
* Rebuild what the lab runs, from source, before it runs.
|
||||
*
|
||||
* **A stale artifact reporting success against old rules is the fault this project keeps writing
|
||||
* down** (novox/hq 04-ISSUES/005). The lab consumes three artifacts from two repositories, and they
|
||||
* down** (novox/hq 04-ISSUES/005). The lab consumes a handful of artifacts from two repositories,
|
||||
* and they
|
||||
* were rebuilt by hand, one at a time, from memory. A rename in the control plane's catalogue needs
|
||||
* both the control-plane image *and* the builder binary, because both parse manifests; rebuilding
|
||||
* one left a binary eleven hours old refusing a field the mesh had just renamed, and cost a full
|
||||
@@ -73,9 +74,40 @@ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// **The installer, carrying the control plane's image.**
|
||||
//
|
||||
// Last, and that is an ordering rather than a preference: `make bootstrap` embeds the output of
|
||||
// `docker save <image>`, so the image has to have been built by the step above or the installer
|
||||
// carries whatever was lying around — the eleven-hour-old artifact again, this time inside a
|
||||
// binary where nothing would ever notice.
|
||||
//
|
||||
// It is built here at all because the bed now bootstraps THROUGH it (novox/hq ADR 0067): the
|
||||
// anchor is brought into existence by running the same program a bare machine runs, rather than
|
||||
// by the bed applying a substrate bundle by hand and calling that an install. An installer that
|
||||
// was stale would be a bed proving something about last week's procedure.
|
||||
const installer = env["MESH_LAB_BOOTSTRAP_BINARY"];
|
||||
if (installer && where["mesh-host"]) {
|
||||
builds.push({
|
||||
what: "installer",
|
||||
in: where["mesh-host"],
|
||||
argv: ["make", "bootstrap", `IMAGE=${controlPlaneImage(env)}`, `BOOTSTRAP_OUT=${installer}`],
|
||||
});
|
||||
}
|
||||
return builds;
|
||||
}
|
||||
|
||||
/**
|
||||
* The control-plane image the installer carries.
|
||||
*
|
||||
* `mesh-control:development` is what mesh-control's `make image` tags, and what the scenarios name
|
||||
* — one tag, said in one place. It is overridable because a release installer carries a release
|
||||
* image, and nothing about that is the lab's business.
|
||||
*/
|
||||
export function controlPlaneImage(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return env["MESH_LAB_CONTROL_IMAGE"] ?? "mesh-control:development";
|
||||
}
|
||||
|
||||
/** rebuild runs the plan, and throws on the first failure rather than testing a stale artifact. */
|
||||
export function rebuild(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const built: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user