The beds name images the way a machine would find them
Twenty-eight integration tests each carried their own copy of the same two helpers, which pointed a manifest and the substrate bundle at whatever the lab's registry had assigned. They now share two in the harness, and the difference is the point: ours is rewritten to the ID the machine holds it under, and everything else is left exactly as written so the machine pulls it. **The substrate bundle is where the fiction was most load-bearing.** mesh-host's `examples/substrate-first-node.lock` pins all three of its images at `192.0.2.250:5000/…`, which is the address the lab's registry served from — it was written for a target, and the target was the lab. Two of those are ordinary third-party images and become the digests mesh-catalog's own postgres and lavinmq modules pin, so the substrate's store and broker are literally the images the mesh runs. mesh-control exists in no registry at all and becomes the ID the machine was handed. **The bundle itself should be fixed in mesh-host and this substitution deleted with it.** Beds that wrote a manifest by hand named an image by repository and let the rewrite supply a digest. There is nothing to supply one now, so `onTheMachine` refuses an unpinned reference and hands back the digest the catalogue pins — a bed runs the image the mesh ships, and a bed that drifts from the catalogue is testing a different postgres. Three beds took a third-party image out of the raised list, which no longer contains one: certificates (pebble), objectstore (minio and its client) and provisioner (postgres) now name theirs and pull it. builds and mesh publish into the MESH's own artifact store — the `registry` module's image, on the node, on 5000 — rather than into scenery the lab raised. That is a different claim, and only one of them exists in production. New unit tests cover what a full raise would otherwise be the only way to check: the routes an egress machine gets (that its gateway is still the path to the rest of the scenario, that a range with no path is unreachable rather than leaked to the uplink, that each family gets its own next hop), which machine is handed which of our images, and the `images:` rule that refuses a third-party entry. The "shipped scenarios are valid" test now loads every scenario rather than two of them. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -42,7 +42,8 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, substrateBundle, onTheMachine } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
const binary = hostBinaryPath();
|
||||
@@ -237,7 +238,7 @@ const OPERATOR_SECRETS: { node: string; module: string; name: string; value: str
|
||||
];
|
||||
|
||||
let instanceId = "";
|
||||
let stocked: string[] = [];
|
||||
let held: HeldImage[] = [];
|
||||
|
||||
function quote(s: string): string {
|
||||
return `'${s.replaceAll("'", `'\\''`)}'`;
|
||||
@@ -263,27 +264,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
||||
return must(CONTROL, `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
|
||||
}
|
||||
|
||||
function repositoryFor(reference: string): string {
|
||||
const withoutDigest = reference.split("@")[0] ?? reference;
|
||||
const lastColon = withoutDigest.lastIndexOf(":");
|
||||
const lastSlash = withoutDigest.lastIndexOf("/");
|
||||
return lastColon > lastSlash ? withoutDigest.slice(0, lastColon) : withoutDigest;
|
||||
/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */
|
||||
function pinned(reference: string): string {
|
||||
return onTheMachine(reference, held);
|
||||
}
|
||||
|
||||
function pinned(repository: string): string {
|
||||
const found = stocked.find((r) => r.slice(r.indexOf("/") + 1, r.indexOf("@")) === repository);
|
||||
assert.ok(found, `the scenario stocks no ${repository}; it serves\n ${stocked.join("\n ")}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
function bundleFor(images: string[]): string {
|
||||
let text = readFileSync(bundle, "utf8");
|
||||
for (const ref of images) {
|
||||
const repository = ref.slice(ref.indexOf("/") + 1, ref.indexOf("@"));
|
||||
const escaped = repository.replaceAll("/", "\\/").replaceAll(".", "\\.");
|
||||
text = text.replaceAll(new RegExp(`[A-Za-z0-9_.:-]+\\/${escaped}@sha256:[0-9a-f]+`, "g"), ref);
|
||||
}
|
||||
return text;
|
||||
function bundleFor(images: HeldImage[]): string {
|
||||
return substrateBundle(bundle, images);
|
||||
}
|
||||
|
||||
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||
@@ -294,7 +281,7 @@ function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||
const remap = REMAP[name] ?? {};
|
||||
for (const r of m.resources ?? []) {
|
||||
if (r.type !== "container") continue;
|
||||
if (typeof r.image === "string") r.image = pinned(repositoryFor(r.image));
|
||||
if (typeof r.image === "string") r.image = pinned(r.image);
|
||||
if (Array.isArray(r.ports)) r.ports = r.ports.map((p) => remap[p] ?? p);
|
||||
}
|
||||
const manifest = JSON.stringify(m);
|
||||
@@ -426,11 +413,14 @@ before(async () => {
|
||||
...(FIXED_ID ? { instanceId: FIXED_ID } : {}),
|
||||
});
|
||||
instanceId = raised.instanceId;
|
||||
stocked = raised.images;
|
||||
held = raised.images;
|
||||
console.log(`INSTANCE ${instanceId}${KEEP ? " (KEEP — will be left standing)" : ""}`);
|
||||
|
||||
// novox raises the substrate from its bundle, digests rewritten to the scenario registry's. This
|
||||
// is the collapse: the substrate rides novox, not a separate anchor. The bundle hardcodes the
|
||||
// novox raises the substrate from its bundle. The store and the broker keep upstream references
|
||||
// and novox PULLS them, over its uplink, the way any first node does; mesh-control exists in no
|
||||
// registry, so it becomes the ID novox holds it under — the whole of what changed here.
|
||||
//
|
||||
// The rest is the collapse: the substrate rides novox, not a separate anchor. The bundle hardcodes the
|
||||
// broker's advertised address as 192.0.2.10:5671 (the OLD separate-anchor address) — and a token
|
||||
// carries MESH_BROKER_ADDRESS verbatim as the endpoint an enrolling node dials. With the substrate
|
||||
// on novox that endpoint must be novox's own public address, or every node (novox included) would
|
||||
@@ -439,12 +429,17 @@ before(async () => {
|
||||
const bundleText = bundleFor(raised.images).replaceAll("192.0.2.10:5671", "192.0.2.20:5671");
|
||||
await must(CONTROL, `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleText}\nMESHBUNDLE`);
|
||||
|
||||
// **Belt as well as braces on the registry.** `raise` now refuses to return until every machine
|
||||
// can fetch a manifest from the scenario registry, so the first attempt should be the only one.
|
||||
// This retry is here because of what the failure looked like when the guarantee was missing: the
|
||||
// apply died on a pull, `before` threw, and the instance was left a bare shell — VMs and a
|
||||
// registry, no substrate, no enrolment, nothing to read. A pull is the one step here that can
|
||||
// fail for a reason that goes away by itself, so it is the one step worth attempting twice.
|
||||
// **Belt as well as braces on fetching.** `raise` refuses to return until every machine with
|
||||
// egress has resolved a name and reached the internet, so the first attempt should be the only
|
||||
// one. This retry is here because of what the failure looked like when there was no such
|
||||
// guarantee: the apply died on a pull, `before` threw, and the instance was left a bare shell —
|
||||
// VMs, no substrate, no enrolment, nothing to read.
|
||||
//
|
||||
// And a pull is now genuinely the one step that can fail for a reason which goes away by itself:
|
||||
// the store and the broker come from the internet, through a household gateway's masquerade, and
|
||||
// a registry elsewhere having a bad minute is not this mesh's fault. That is the trade this bed
|
||||
// accepts — it is no longer hermetic, because a real node is not either, and the faults it was
|
||||
// hiding were exactly the ones that only appear when a machine has to fetch for itself.
|
||||
{
|
||||
let applied = false;
|
||||
let said = "";
|
||||
|
||||
Reference in New Issue
Block a user