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:
2026-09-10 23:16:41 +02:00
parent 5c91c0ecd2
commit 675facdb0d
40 changed files with 898 additions and 705 deletions
+128
View File
@@ -0,0 +1,128 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseScenario } from "../src/declaration/parse.ts";
import { scenarioRoutesFor } from "../src/lifecycle/address.ts";
/**
* The uplink and the declared gateway must not fight.
*
* **This is the one decision the registry's removal turned on, and it is invisible in a raise.**
* Every machine that needs an image now has an `egress` uplink, and the uplink's DHCP offers a
* default route. So did the scenario: a machine behind a household gateway defaulted through it, a
* machine on a public segment defaulted through transit. Both of those are containers that reach
* the scenario and nothing else — no route to the real internet, by design, because they exist to
* reproduce a household router rather than to be one.
*
* A default route through either is therefore a black hole for anything outside, and it beats the
* uplink's route on metric. The machine would sit failing every pull with a routing table that
* looks perfectly reasonable.
*
* The answer is that an egress machine states the scenario's ranges explicitly and lets the uplink
* be the default. These tests are how that is checked without spending an hour raising four nodes.
*/
const HOUSEHOLD = `
scenario: household
segments:
hosting:
kind: public
cidr: [192.0.2.0/24]
home:
kind: private
cidr: [192.168.1.0/24]
gateway:
to: hosting
address: [192.0.2.50]
nat: [v4]
forwardable: true
machines:
novox:
at: { segment: hosting, address: [192.0.2.20] }
egress: true
ace:
at: { segment: home, address: [192.168.1.10] }
egress: true
sealed:
at: { segment: home, address: [192.168.1.99] }
`;
test("a machine behind a gateway still reaches the scenario through that gateway", () => {
// The whole point of the topology: home→public is a masqueraded outbound path, and the overlay
// handshake has to survive it. An egress machine that stopped using its gateway would be
// testing a flat network with extra steps.
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace");
assert.deepEqual(routes, [{ cidr: "192.0.2.0/24", via: "192.168.1.1" }]);
});
test("a machine's own segment gets no route — it is already on-link", () => {
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace");
assert.ok(!routes.some((r) => r.cidr === "192.168.1.0/24"), JSON.stringify(routes));
});
/**
* **The dangerous one.** `home` is 192.168.1.0/24 — a documentation range in spirit, an ordinary
* private one in fact, and very possibly the network the workstation itself is on.
*
* With one public segment there is no transit router, so novox has no path to `home` at all. Left
* to fall through, that traffic would leave by the uplink and land on whatever the workstation can
* reach. Unreachable is both the faithful reproduction of what it had before — a default route into
* scenery that dropped it — and the only safe answer.
*/
test("a range with no path inside the scenario is unreachable, not leaked to the uplink", () => {
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "novox");
assert.deepEqual(routes, [{ cidr: "192.168.1.0/24", via: null }]);
});
test("a machine without egress is left to its default route, and states nothing", () => {
// Not because it needs no routes — it has one, a default through its gateway, applied the old
// way. This function is only asked about machines whose default belongs to the uplink.
const scenario = parseScenario(HOUSEHOLD);
assert.equal(scenario.machines["sealed"]?.egress, undefined);
});
const TWO_PUBLIC = `
scenario: two-public
segments:
hosting:
kind: public
cidr: [192.0.2.0/24]
elsewhere:
kind: public
cidr: [198.51.100.0/24]
machines:
anchor:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
`;
test("with a second public segment the transit router is the way across, as it always was", () => {
// Transit is raised only when there is more than one public segment, so this is exactly the
// case where pointing at it means something.
const routes = scenarioRoutesFor(parseScenario(TWO_PUBLIC), "anchor");
assert.deepEqual(routes, [{ cidr: "198.51.100.0/24", via: "192.0.2.254" }]);
});
const V6 = `
scenario: both-families
segments:
hosting:
kind: public
cidr: [192.0.2.0/24, "2001:db8:a::/48"]
elsewhere:
kind: public
cidr: [198.51.100.0/24, "2001:db8:b::/48"]
machines:
anchor:
at: { segment: hosting, address: [192.0.2.10, "2001:db8:a::10"] }
egress: true
`;
test("each family is routed through its own next hop", () => {
// A v6 range routed via a v4 next hop is not a route, and the reverse is not either.
const routes = scenarioRoutesFor(parseScenario(V6), "anchor");
assert.deepEqual(routes, [
{ cidr: "198.51.100.0/24", via: "192.0.2.254" },
{ cidr: "2001:db8:b::/48", via: "2001:db8:a::fffe" },
]);
});
+9 -15
View File
@@ -49,7 +49,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();
@@ -73,7 +74,7 @@ const ACCESS_TOKEN = "at-lab-access-token-minted-by-the-stub";
const ROTATED_REFRESH = "rt-lab-rotated-still-only-the-manager";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -113,20 +114,13 @@ async function meshTry(command: string): Promise<{ out: string; ok: boolean }> {
return on(`docker exec mesh-control /mesh-control ${command}`);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -177,7 +171,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+14 -20
View File
@@ -7,7 +7,7 @@
* container that connects over amqps with that account — never the broker's own. The trail filling
* is the proof the delivered, scoped credential authenticated and the subscription bound.
*
* It needs the host binary, the substrate bundle, and the runtime image stocked by the scenario:
* It needs the host binary, the substrate bundle, and the runtime image the scenario loads:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
@@ -22,7 +22,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();
@@ -40,8 +41,8 @@ const SCENARIO = "audit-node";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -67,22 +68,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -125,7 +119,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
@@ -151,7 +145,7 @@ after(async () => {
test("the mesh assigns the audit logger, and it consumes over the account the mesh delivered", {
skip, timeout: 900_000,
}, async () => {
// The assigned-module manifest (mesh-catalog), its runtime image the digest this registry serves.
// The assigned-module manifest (mesh-catalog), its runtime image the ID the machine holds.
const manifest = JSON.stringify({
module: "audit-logger",
version: "1",
@@ -30,7 +30,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();
@@ -48,8 +49,8 @@ const SCENARIO = "catalogue-apps";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -75,22 +76,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -133,7 +127,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
@@ -26,7 +26,8 @@
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* scripts/build-module-runtime.sh {sonarr,radarr} build the runtime images into the local daemon;
* scenarios/catalogue-media.yml stocks them. lscr.io/linuxserver/{sonarr,radarr} must be in the
* local daemon to be stocked. Each *arr runtime is given a lab API key so its client constructs and
* local daemon; the service images are pulled from the internet. Each *arr runtime is given a lab
* API key so its client constructs and
* its tools register (as plex is given a lab token) — the server need not be configured by hand.
*/
@@ -37,7 +38,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();
@@ -55,8 +57,8 @@ const SCENARIO = "catalogue-media";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -82,22 +84,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -140,7 +135,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
@@ -26,7 +26,7 @@
* scripts/build-module-runtime.sh mosquitto builds mesh-runtime-mosquitto:development (carrying
* mosquitto_ctrl and the compiled bootstrap entrypoint) into the local daemon, which
* scenarios/catalogue-mqtt.yml stocks. eclipse-mosquitto:2 must be in the local daemon to be
* stocked; the host pulls both from the scenario's own registry by digest.
* the host pulls both from the internet over its uplink, by the digests the catalogue pins.
*/
import { test, before, after } from "node:test";
@@ -36,7 +36,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();
@@ -54,7 +55,7 @@ const SCENARIO = "catalogue-mqtt";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -80,22 +81,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -138,7 +132,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
@@ -21,7 +21,7 @@
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* scripts/build-module-runtime.sh {postgres,redis,minio,plex} build the four runtime images into the
* local daemon; scenarios/catalogue-small.yml stocks them. postgres:17-alpine, redis:7-alpine and
* minio/minio:latest must be in the local daemon to be stocked.
* minio/minio:latest is pulled from the internet by the node itself.
*/
import { test, before, after } from "node:test";
@@ -31,7 +31,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();
@@ -49,8 +50,8 @@ const SCENARIO = "catalogue-small";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -76,22 +77,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -140,7 +134,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
+9 -15
View File
@@ -22,7 +22,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();
@@ -40,7 +41,7 @@ const SCENARIO = "grafana-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -65,20 +66,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -121,7 +115,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+12 -18
View File
@@ -38,7 +38,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();
@@ -58,8 +59,8 @@ const SCENARIO = "model-usage-bed";
const NODE = "laptop";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -85,22 +86,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -164,7 +158,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+13 -19
View File
@@ -10,7 +10,7 @@
* proof the invocation routed to the assigned runtime, ran plex's real code, and replied, all under
* the scoped account and never the broker's own.
*
* It needs the host binary, the substrate bundle, and the runtime image stocked by the scenario:
* It needs the host binary, the substrate bundle, and the runtime image the scenario loads:
*
* MESH_LAB_HOST_BINARY=.../mesh-host
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
@@ -25,7 +25,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();
@@ -43,8 +44,8 @@ const SCENARIO = "plex-node";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -70,22 +71,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -128,7 +122,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
+10 -16
View File
@@ -12,7 +12,7 @@
* has no way yet to deliver one to a provider's runtime (04-ISSUES). The manifest here sets a
* lab-local key so the mechanism can be proven; the delivery is a separate, open design question.
*
* It needs the host binary, the substrate bundle, and the runtime image stocked by the scenario:
* It needs the host binary, the substrate bundle, and the runtime image the scenario loads:
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
* scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development into the local
@@ -26,7 +26,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();
@@ -44,7 +45,7 @@ const SCENARIO = "redis-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -69,20 +70,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -125,7 +119,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+12 -18
View File
@@ -38,7 +38,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();
@@ -56,8 +57,8 @@ const SCENARIO = "schedule-tick";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -83,22 +84,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -155,7 +149,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
+9 -15
View File
@@ -20,7 +20,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();
@@ -38,7 +39,7 @@ const SCENARIO = "sonarr-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -63,20 +64,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -119,7 +113,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
@@ -29,7 +29,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();
@@ -47,8 +48,8 @@ const SCENARIO = "tools-confluence";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -74,22 +75,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -132,7 +126,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
+12 -18
View File
@@ -28,7 +28,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();
@@ -46,8 +47,8 @@ const SCENARIO = "tools-gitlab";
const MACHINE = "anchor";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -73,22 +74,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -131,7 +125,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
+12 -18
View File
@@ -44,7 +44,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();
@@ -63,8 +64,8 @@ const SCENARIO = "two-node-db";
const NODE = "laptop";
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -90,22 +91,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -173,7 +167,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// The first node raises the substrate — store, broker, control — from the bundle its host carries,
// its digests rewritten to the ones this scenario's own registry serves.
+35 -14
View File
@@ -20,7 +20,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 } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
@@ -42,7 +43,22 @@ const skip = !capability.usable
const SCENARIO = "first-node";
const MACHINE = "anchor";
let instanceId = "";
let registry = "";
/**
* Where a build publishes to.
*
* **The mesh has a registry, and this is that one.** `mesh-catalog/modules/registry` serves the
* mesh's artifact store on port 5000; a build publishes into it. This test starts the same image
* on the machine directly rather than assigning the module, because what is under test is the
* build chain and not module delivery.
*
* It used to publish into the registry the LAB raised inside the scenario — scenery pretending to
* be upstream, which is the thing this change removed. A registry the mesh runs and a registry the
* lab runs are different claims, and only the first exists in production.
*/
const ARTIFACT_STORE =
"registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373";
const registry = "127.0.0.1:5000";
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -66,28 +82,33 @@ async function mesh(command: string): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`);
}
/** The bundle, pointed at this scenario's own registry. */
function bundleFor(images: string[]): string {
let text = readFileSync(bundle, "utf8");
for (const pinned of images) {
const repository = pinned.slice(pinned.indexOf("/") + 1, pinned.indexOf("@"));
const escaped = repository.replaceAll("/", "\\/").replaceAll(".", "\\.");
text = text.replaceAll(new RegExp(`[A-Za-z0-9_.:-]+\\/${escaped}@sha256:[0-9a-f]+`, "g"), pinned);
}
return text;
/** The bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
before(async () => {
if (skip) return;
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {});
instanceId = raised.instanceId;
const first = raised.images[0];
assert.ok(first, "the scenario stocked no images, so there is no registry to publish to");
registry = first.slice(0, first.indexOf("/"));
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`);
// The mesh's artifact store, standing where the `registry` module would. Read back rather than
// assumed: a builder publishing into a registry that never came up fails several minutes later,
// as a manifest naming a blob nobody has.
await must(
`docker run -d --name mesh-registry --restart unless-stopped ` +
`-p ${registry}:5000 ${ARTIFACT_STORE}`,
);
let serving = false;
for (let i = 0; i < 30 && !serving; i++) {
({ ok: serving } = await on(`curl -sf http://${registry}/v2/ >/dev/null`));
if (!serving) await new Promise((r) => setTimeout(r, 2_000));
}
assert.ok(serving, "the mesh's artifact store never answered, so a build has nowhere to publish");
// A module repository on the machine. Local rather than fetched, because what is under test is
// the mesh's chain and not whether the lab can reach a forge.
await must(`mkdir -p /root/shell/files`);
+9 -2
View File
@@ -32,6 +32,14 @@ const SCENARIO = "a-public-name";
const MACHINE = "anchor";
const NAME = "photos.example";
const ACME = "/var/lib/acme";
/**
* The ACME server under test, pulled by the machine over its uplink.
*
* It used to be served from a registry the lab raised inside the scenario. Nothing outside the lab
* has one, so an image only reachable there was a fiction — and this test is about a certificate
* being obtained over a real path.
*/
const AUTHORITY = "ghcr.io/letsencrypt/pebble:2.5.0";
let instanceId = "";
@@ -59,8 +67,7 @@ before(async () => {
const instance = await raise(scenario, {});
instanceId = instance.instanceId;
const pebble = instance.images.find((r) => r.includes("pebble"));
assert.ok(pebble, `the scenario stocked no ACME server: ${instance.images.join(", ")}`);
const pebble = AUTHORITY;
await must(`mkdir -p ${ACME}/cache`);
+6 -13
View File
@@ -33,7 +33,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 } from "./harness.ts";
import type { HeldImage } from "../../src/pinning.ts";
import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
@@ -97,17 +98,8 @@ async function must(command: string, timeoutMs?: number): Promise<string> {
* is not this one; matching by repository and rewriting to the digest this registry assigned is
* what makes it applicable (the same rewrite mesh.test.ts does).
*/
function bundleFor(images: string[]): string {
let text = readFileSync(bundle, "utf8");
for (const pinned of images) {
const repository = pinned.slice(pinned.indexOf("/") + 1, pinned.indexOf("@"));
const escaped = repository.replaceAll("/", "\\/").replaceAll(".", "\\.");
text = text.replaceAll(
new RegExp(`[A-Za-z0-9_.:-]+\\/${escaped}@sha256:[0-9a-f]+`, "g"),
pinned,
);
}
return text;
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
/** Read the trail back as parsed JSON lines. */
@@ -129,7 +121,8 @@ before(async () => {
instanceId = raised.instanceId;
// The node raises its substrate — store, broker and the rest — from the bundle, applied from a
// file because the digests are this registry's and are not known until it is up.
// file because the control plane's image is named by the ID this machine holds it under,
// which is not knowable until it has been handed over.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+106
View File
@@ -9,11 +9,117 @@
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { isReachable, pools, supportedDrivers } from "../../src/incus/client.ts";
import { destroy, list } from "../../src/lifecycle/operate.ts";
import { diagramFromLive } from "../../src/diagram/from-live.ts";
import { duplicateAddresses, describeConflicts, type Held } from "../../src/lifecycle/invariants.ts";
import type { Scenario } from "../../src/declaration/types.ts";
import { isMeshBuilt, pinnedInto, referenceFor, repositoryOf, type HeldImage } from "../../src/pinning.ts";
// --- the substrate bundle, and what its three images are on a real machine ---------------------
/**
* The example bundle in mesh-host names a registry that no longer exists.
*
* `examples/substrate-first-node.lock` was written **for a target**, and the target was the lab: it
* pins `192.0.2.250:5000/…` because that is where the registry the lab used to raise served from.
* That registry is gone, so those three references name nothing.
*
* Two of them are ordinary third-party images and belong to the internet. Rather than invent
* digests here, they are the ones the mesh's own modules already pin — mesh-catalog's `postgres`
* and `lavinmq` — so the substrate's store and broker are literally the images the mesh runs. The
* third, mesh-control, exists in no registry at all and becomes the ID the machine holds it under.
*
* **The bundle itself should be fixed in mesh-host**, and this substitution deleted with it. It is
* here because the file lives in another repository and because a fixture that lies about where an
* image comes from is exactly what this change is removing.
*/
const UPSTREAM_STORE =
"postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee";
const UPSTREAM_BROKER =
"cloudamqp/lavinmq@sha256:3eb54c12916d700a978c2ea86e6362cd4974b0e3189508718006d4e6d341246b";
/**
* The substrate bundle as a machine should receive it.
*
* Third-party references become upstream ones, which the machine pulls over its uplink; ours
* become the ID the machine was handed. Nothing points inside the scenario any more, which is the
* whole of this change: what the bed proves about a bootstrap is now what would happen anywhere.
*/
export function substrateBundle(path: string, held: HeldImage[]): string {
let text = readFileSync(path, "utf8");
text = text.replaceAll(/[A-Za-z0-9_.:-]+\/postgres@sha256:[0-9a-f]{64}/g, UPSTREAM_STORE);
text = text.replaceAll(
/[A-Za-z0-9_.:-]+\/cloudamqp\/lavinmq@sha256:[0-9a-f]{64}/g, UPSTREAM_BROKER);
return pinnedInto(text, held);
}
/**
* The upstream reference for a third-party image, as the mesh's own catalogue pins it.
*
* A bed that writes a manifest by hand still has to name an image exactly — mesh-host refuses a
* tag, and rightly (novox/hq ADR 0006). While the lab had a registry the beds sidestepped that by
* naming a repository and letting the rewrite supply a digest; there is nothing to supply one now,
* so the digest has to be written down.
*
* These are the digests mesh-catalog's own modules pin, taken from `mesh-catalog/modules/*` — so a
* bed runs the image the mesh runs, and a bed that drifts from the catalogue is a bed testing a
* different postgres than the mesh ships.
*/
const UPSTREAM = new Map<string, string>([
["alpine", "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"],
["baserow/baserow", "baserow/baserow@sha256:834424a10413798567f76428f255dc259445b7f8dcec56598c05b4073bb2a124"],
["cloudamqp/lavinmq", "cloudamqp/lavinmq@sha256:3eb54c12916d700a978c2ea86e6362cd4974b0e3189508718006d4e6d341246b"],
["eclipse-mosquitto", "eclipse-mosquitto@sha256:6f8d8a947c506f8a2290ec65cd4bd2bc7cb4d43fb5f6271f861cb013e2ef9797"],
["ghcr.io/umami-software/umami", "ghcr.io/umami-software/umami@sha256:fa32d116cf20cad52cbc3fad9a63b46e7fa02299d8f967168eb453d49c476b4a"],
["letta/letta", "letta/letta@sha256:1d2e0692514287c5ed1a483e14e16ed945f8632d315539f5e66373bb7d7c471b"],
["lscr.io/linuxserver/radarr", "lscr.io/linuxserver/radarr@sha256:119aaa4a4f7349bcd2a136c5373a0d7925b5479915c7dfe0c0ad352db2a6d438"],
["lscr.io/linuxserver/sonarr", "lscr.io/linuxserver/sonarr@sha256:c19aa4ecdf03d73e1d5c901da33744cb7eb4d921f89bafed1ca264601d7fa224"],
["lscr.io/linuxserver/unifi-controller", "lscr.io/linuxserver/unifi-controller@sha256:fcd5d8b13a77a588c79c1b49e5fc9ad08115aa3bb1a3576c589c64908a68845f"],
["minio/minio", "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2"],
["mongo", "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3"],
["ollama/ollama", "ollama/ollama@sha256:32931b46719f673c05fdbaa81ccb26da18ea4a1c57590a754874ab28ba269eb2"],
["postgres", "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee"],
["redis", "redis@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf"],
["registry", "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373"],
["synesthesiam/marytts", "synesthesiam/marytts@sha256:45970ecb3e21a2981c66c60563a70cf00be8e95c02565e7d74b3a73dcec7db2c"],
]);
/**
* What a manifest's image reference becomes on the machine.
*
* Three cases, and the middle one is the whole change:
*
* - **Ours** becomes the ID the machine holds it under. Nothing serves it, and nothing needs to.
* - **Anything already pinned by digest** is returned exactly as written. The machine pulls it
* from the internet, over its uplink, which is what a real machine does and what the lab spent
* a long time serving from a registry of its own instead.
* - **A bare repository or tag** is one of ours in spirit — a bed naming an image by hand — and
* is given the digest the catalogue pins. A tag would be refused by mesh-host anyway, and
* refusing here says why rather than failing on the machine.
*/
export function onTheMachine(reference: string, held: HeldImage[]): string {
if (isMeshBuilt(reference)) {
const found = referenceFor(held, repositoryOf(reference));
assert.ok(
found,
`nothing loaded ${reference} onto the machines. They hold:\n ` +
held.map((i) => `${i.repository} ${i.reference}`).join("\n "),
);
return found;
}
if (reference.includes("@sha256:")) return reference;
const upstream = UPSTREAM.get(repositoryOf(reference));
assert.ok(
upstream,
`${reference} is not pinned and this harness does not know an upstream digest for it. ` +
`Add the one mesh-catalog pins, or write the reference out in full — a tag moves, and the ` +
`host refuses one.`,
);
return upstream;
}
export interface Capability {
usable: boolean;
+11 -17
View File
@@ -41,7 +41,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();
@@ -62,7 +63,7 @@ const NODE = "laptop";
const CONSUMER_LOGIN = "mesh_laptop_ping";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -88,22 +89,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -161,7 +155,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+9 -15
View File
@@ -26,7 +26,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();
@@ -44,7 +45,7 @@ const SCENARIO = "local-model-bed";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -73,20 +74,13 @@ async function meshTry(command: string): Promise<{ out: string; ok: boolean }> {
return on(`docker exec mesh-control /mesh-control ${command}`);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -136,7 +130,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+9 -15
View File
@@ -23,7 +23,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();
@@ -41,7 +42,7 @@ const SCENARIO = "redis-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -66,20 +67,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -122,7 +116,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+42 -49
View File
@@ -21,10 +21,10 @@ import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { pinnedInto, stillUnpinned } from "../../src/pinning.ts";
import { pinnedInto, stillUnpinned, type HeldImage } from "../../src/pinning.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 { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
import { ready, returnTo, keep, rememberStock, warmStock } from "../../src/warm.ts";
@@ -49,23 +49,27 @@ const skip = !capability.usable
const SCENARIO = "two-nodes";
let instanceId = "";
/** The scenario's own registry, which serves the images a module may mirror. */
let registry = "";
/** What that registry actually serves, by repository. */
let stocked: string[] = [];
/**
* The pinned reference for one of the scenario's images.
* The MESH's own artifact store, once the registry module is running on the anchor.
*
* By digest, because the lab's registry drops tags when it stocks: `registry:2` is not there and
* asking for it fails with "not found", which reads like a missing image rather than a naming
* convention. A digest is also what a declaration pins, so this is the reference a module would
* really carry.
* Not a registry the lab raised — there is no longer any such thing. A build publishes into the
* store the mesh itself runs, which is the only registry that exists outside this repository.
*/
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 ${stocked.join(", ")}`);
return found;
const registry = "127.0.0.1:5000";
/**
* The registry module's image, pinned upstream, pulled by the machine over its uplink.
*
* The digest mesh-catalog's `registry` module pins, so the store the mesh runs here is the store
* the mesh runs anywhere.
*/
const ARTIFACT_STORE =
"registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373";
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
/** 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 quote(s: string): string {
@@ -179,23 +183,14 @@ async function settled(node: string, withinMs = 480_000): Promise<void> {
}
/**
* The bundle, with every image reference pointed at this scenario's registry.
* The bundle, as a machine should receive it.
*
* Matched by repository rather than by the whole reference, because the address and the digest
* both differ from whatever the committed bundle names — and a bundle that names the wrong
* registry is not wrong, it is built for a different target.
* The committed example was written for a target that had a registry the lab raised. Its two
* third-party images become upstream references the machine pulls itself; mesh-control, which
* exists in no registry, becomes the ID this machine was handed.
*/
function bundleFor(images: string[]): string {
let text = readFileSync(bundle, "utf8");
for (const pinned of images) {
const repository = pinned.slice(pinned.indexOf("/") + 1, pinned.indexOf("@"));
const escaped = repository.replaceAll("/", "\\/").replaceAll(".", "\\.");
text = text.replaceAll(
new RegExp(`[A-Za-z0-9_.:-]+\\/${escaped}@sha256:[0-9a-f]+`, "g"),
pinned,
);
}
return text;
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
/** Take a token out of what `token issue` printed. It is the one base64url blob on its own line. */
@@ -219,7 +214,7 @@ before(async () => {
if (said.use === "restore") {
instanceId = said.instanceId;
const seconds = await returnTo(instanceId);
stocked = warmStock(instanceId).images;
held = warmStock(instanceId).images;
// **A snapshot captures disk, not memory.** Restoring reboots the machine, so everything
// this suite started by hand is gone — the host most of all. Without it the mesh looks
@@ -255,13 +250,11 @@ before(async () => {
instanceId = raised.instanceId;
// The first node raises everything from a file rather than from a bundle built into the binary,
// because the digests are this registry's and are not known until it is up.
// because the control plane's image is named by the ID this machine holds it under, which is not
// knowable until it has been handed over.
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`);
stocked = raised.images;
const first = raised.images[0];
assert.ok(first, "the scenario stocked no images, so nothing can be mirrored");
registry = first.slice(0, first.indexOf("/"));
// A build machine, so anything here can ask the mesh to build something. Placed rather than
// assumed: nothing else in this scenario would start one.
@@ -279,7 +272,7 @@ before(async () => {
if (warming) {
// Snapshotted only now, with everything up: a state worth returning to is the one after the
// part nobody wants to repeat.
await rememberStock(instanceId, stocked);
await rememberStock(instanceId, held);
const warm = await keep(SCENARIO, instanceId);
console.log(`warm: ${warm.instanceId} kept, against ` +
Object.entries(warm.against).map(([n, c]) => `${n} ${c}`).join(", "));
@@ -608,13 +601,13 @@ test("a machine that fell behind catches up without being named", { skip, timeou
});
test("the mesh runs its own artifact store", { skip, timeout: 900_000 }, async () => {
// Artifacts go to a registry, and the only registries that existed were raised by the lab or by
// the bootstrap bundle. A mesh had no way to run its own.
// Artifacts go to a registry, and a mesh had no way to run its own — the only one that existed
// was raised by the lab, which is to say it existed nowhere but here.
//
// **Named, not mirrored** (novox/hq 04-ISSUES/029). Mirroring publishes to the artifact store,
// and the builder will not start without one — so a module that provides the store and builds
// its own image asks the mesh to put an artifact into the thing that artifact is needed to
// create. It worked here only because the scenario's registry was already standing to receive
// create. It used to pass here only because the LAB's registry was already standing to receive
// the push, which is exactly why a real first mesh would have found this and the lab did not.
//
// So the image is named by digest, the way the bundle names the three a first node starts from.
@@ -626,7 +619,7 @@ test("the mesh runs its own artifact store", { skip, timeout: 900_000 }, async (
`"serves":{"artifact-store":{"port":5000}},` +
`"resources":[` +
`{"id":"state","type":"directory","path":"/var/lib/mesh/registry","mode":"0700"},` +
`{"id":"store","type":"container","name":"mesh-registry","image":"${pinned("registry")}",` +
`{"id":"store","type":"container","name":"mesh-registry","image":"${ARTIFACT_STORE}",` +
`"ports":["5000:5000"],"volumes":["mesh-registry-data:/var/lib/registry"]}]}' ` +
`> /root/registry/module.json`);
// **Added, not built** — and this is the half that proves the fix. Building needs a builder,
@@ -677,7 +670,7 @@ test("a machine serves its internal name with a certificate the mesh issued", {
// The name it was issued for is the one the mesh gave this machine.
const named = await must("anchor",
`openssl x509 -in /etc/mesh/serving.crt -noout -ext subjectAltName 2>/dev/null || ` +
`docker run --rm -v /etc/mesh:/m ${pinned("registry")} sh -c ` +
`docker run --rm -v /etc/mesh:/m ${ARTIFACT_STORE} sh -c ` +
`"apk add --no-cache openssl >/dev/null 2>&1; openssl x509 -in /m/serving.crt -noout -text" | grep -A1 'Alternative'`);
assert.match(named, /anchor\.internal/, `the certificate is not for this machine's name:\n${named}`);
@@ -1086,7 +1079,7 @@ test("a route is a grant: a workload is reached by the name it asked for", {
`"listens":[{"port":8088,"from":"mesh","why":"the proxy reaches it here"}],` +
`"resources":[{"id":"dir","type":"directory","path":"/etc/storefront","mode":"0755"},` +
`{"id":"app","type":"container","name":"storefront",` +
`"image":"${pinned("registry")}","ports":["8088:5000"]}]}' > /tmp/storefront.json`);
`"image":"${ARTIFACT_STORE}","ports":["8088:5000"]}]}' > /tmp/storefront.json`);
for (const f of ["frontdoor", "storefront"]) {
await must("anchor", `docker cp /tmp/${f}.json mesh-control:/${f}.json`);
await mesh(`module add /${f}.json`);
@@ -1456,7 +1449,7 @@ test("a container reaches another machine by the name the mesh gave it", {
await must("anchor", `printf %s '{"module":"resolves","version":"1",` +
`"capabilities":["container-runtime"],` +
`"resources":[{"id":"idle","type":"container","name":"resolves",` +
`"image":"${pinned("registry")}"}]}' > /tmp/resolves.json`);
`"image":"${ARTIFACT_STORE}"}]}' > /tmp/resolves.json`);
await must("anchor", `docker cp /tmp/resolves.json mesh-control:/resolves.json`);
await mesh("module add /resolves.json");
await mesh("assign laptop resolves");
@@ -1810,7 +1803,7 @@ test("the real modules resolve together, and compose a declaration a host accept
// until it is built — so the file legitimately carries a placeholder, and composing a
// declaration from it is refused (novox/hq 04-ISSUES/025). Planning what could never run is
// what this test used to do.
const pinned = pinnedInto(raw, stocked);
const pinned = pinnedInto(raw, held);
// What this scenario does not serve cannot be redirected, and a module still naming a
// placeholder cannot be planned — the refusal is the point (novox/hq 04-ISSUES/025). Skipped
// and said, rather than silently dropped: a planning test quietly covering four modules
@@ -1934,8 +1927,8 @@ test("the forge runs, on a database the mesh gave it", { skip, timeout: 900_000
for (const name of ["postgres", "gitea"]) {
const raw = readFileSync(`${process.env["MESH_LAB_MODULES"]}/${name}.json`, "utf8");
// An image the mesh builds has no digest until it is built, and one it does not build belongs
// to whichever registry served it. Both are answered by this scenario's own registry.
const pinned = pinnedInto(raw, stocked);
// to whichever registry served it. Only the first is rewritten; the second is pulled.
const pinned = pinnedInto(raw, held);
assert.deepEqual(stillUnpinned(pinned), [],
`${name} still names an image nothing serves, so it could not start`);
await must("anchor", `printf %s ${quote(pinned)} > /run-${name}.json`);
@@ -2021,7 +2014,7 @@ test("a consumer's cache grant means exactly its own keys", { skip, timeout: 600
// keyspace, so the grant is a pattern — and the test is that the pattern means what the
// manifest said, in both directions.
const raw = readFileSync(`${process.env["MESH_LAB_MODULES"]}/redis.json`, "utf8");
const pinned = pinnedInto(raw, stocked);
const pinned = pinnedInto(raw, held);
assert.deepEqual(stillUnpinned(pinned), [],
"redis still names an image nothing serves, so it could not start");
await must("anchor", `printf %s ${quote(pinned)} > /run-redis.json`);
@@ -21,7 +21,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();
@@ -42,7 +43,7 @@ const SCENARIO = "minio-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -67,20 +68,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -129,7 +123,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+13 -13
View File
@@ -45,8 +45,16 @@ const ROOT_PASSWORD_FILE = "/var/lib/objectstore/root.secret";
const ENDPOINT = "http://127.0.0.1:9000";
let instanceId = "";
/** The store's image, by digest, from the registry the scenario raised. */
let storeImage = "";
/**
* The store and the vendor's client, pinned upstream and pulled by the machine over its uplink.
*
* The store is the digest the mesh's own minio module pins, so this is the store the mesh runs.
* Both used to come from a registry the lab raised inside the scenario; no production mesh has
* one, so a test that could only fetch from it was proving something about the lab.
*/
const storeImage =
"minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2";
const clientImage = "minio/mc:RELEASE.2025-08-13T08-35-41Z";
function shellQuote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -141,18 +149,10 @@ before(async () => {
const instance = await raise(scenario, {});
instanceId = instance.instanceId;
// From the registry the scenario raised, by digest. There is no route to a public registry from
// a documentation range, which is the point of the lab having its own.
const store = instance.images.find((r) => r.includes("minio/minio"));
const client = instance.images.find((r) => r.includes("minio/mc"));
assert.ok(store, `the scenario stocked no store image: ${instance.images.join(", ")}`);
assert.ok(client, `the scenario stocked no client image: ${instance.images.join(", ")}`);
storeImage = store;
// The client, taken out of the vendor's own image onto the machine. The provisioner drives it,
// so it has to be here — and taking it from the stocked image is what keeps this test off any
// public network.
await must(`docker create --name mc-source ${client}`);
// so it has to be here. The machine pulls the image itself, over its uplink, the way it pulls
// everything third-party.
await must(`docker create --name mc-source ${clientImage}`);
await must(`docker cp mc-source:/usr/bin/mc /usr/local/bin/mc && chmod 755 /usr/local/bin/mc`);
await must(`docker rm mc-source`);
+9 -15
View File
@@ -24,7 +24,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();
@@ -46,7 +47,7 @@ const MACHINE = "anchor";
const API_KEY = "sk-lab-openai-static-key-value-for-the-bed-only";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -75,20 +76,13 @@ async function meshTry(command: string): Promise<{ out: string; ok: boolean }> {
return on(`docker exec mesh-control /mesh-control ${command}`);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -139,7 +133,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
@@ -21,7 +21,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();
@@ -39,7 +40,7 @@ const SCENARIO = "postgres-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -64,20 +65,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -120,7 +114,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
@@ -20,7 +20,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();
@@ -38,7 +39,7 @@ const SCENARIO = "redis-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -63,20 +64,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -119,7 +113,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
@@ -24,7 +24,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();
@@ -42,7 +43,7 @@ const SCENARIO = "redis-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -67,20 +68,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -123,7 +117,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+9 -8
View File
@@ -34,8 +34,15 @@ const GRANTS = "/var/lib/postgres/grants";
const SUPER = "postgres://postgres:super@127.0.0.1:5432/postgres?sslmode=disable";
let instanceId = "";
/** The postgres image, by digest, from the registry the scenario raised. */
let image = "";
/**
* The database, pinned upstream and pulled by the machine over its uplink.
*
* The same digest the mesh's own postgres module pins, so this is the database the mesh runs
* rather than a lookalike. It used to come from a registry the lab raised inside the scenario;
* nothing outside the lab has one, so what that proved about fetching an image was true only here.
*/
const image =
"postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee";
function shellQuote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -142,12 +149,6 @@ before(async () => {
const instance = await raise(scenario, {});
instanceId = instance.instanceId;
// From the registry the scenario raised, by digest. There is no route to a public registry from
// a documentation range, which is the point of the lab having its own.
const stocked = instance.images.find((r) => r.includes("postgres"));
assert.ok(stocked, `the scenario stocked no postgres image: ${instance.images.join(", ")}`);
image = stocked;
await must(
`docker run -d --name mesh-db -e POSTGRES_PASSWORD=super ` +
`-p 127.0.0.1:5432:5432 ${image}`,
+11 -17
View File
@@ -37,7 +37,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();
@@ -57,7 +58,7 @@ const NAME = "hello.example";
const PAGE = "hello from hello-web, routed by the mesh";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -83,22 +84,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The pinned reference for one of the scenario's images, by repository. */
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 ${stocked.join(", ")}`);
return found;
/** The reference a manifest should carry, once this scenario has been raised. */
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
function tokenFrom(said: string): string {
@@ -147,7 +141,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// Raise the substrate — store, broker, control — from the bundle.
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
@@ -24,7 +24,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();
@@ -42,7 +43,7 @@ const SCENARIO = "grafana-node";
const MACHINE = "anchor";
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -67,20 +68,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must(`docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
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 ${stocked.join(", ")}`);
return found;
/** 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 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 tokenFrom(said: string): string {
@@ -133,7 +127,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must(`cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must(`${HOST_PATH} apply /tmp/substrate.lock`, 600_000);
+11 -24
View File
@@ -17,7 +17,7 @@
* apps unifi portainer
*
* Each committed module.json is LOADED from mesh-catalog (not hand-written); its container image
* references are rewritten to what this scenario's own registry serves by digest, and the co-located
* references of OURS are rewritten to the IDs the machine holds, and the co-located
* host-port collisions are remapped at load time (see REMAP).
*
* MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
@@ -31,7 +31,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();
@@ -148,7 +149,7 @@ const REMAP: Record<string, Record<string, string>> = {
};
let instanceId = "";
let stocked: string[] = [];
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -173,27 +174,13 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must("anchor", `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 } {
@@ -204,7 +191,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);
@@ -259,7 +246,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`, 900_000);
+26 -31
View File
@@ -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 = "";
+12 -26
View File
@@ -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();
@@ -160,8 +161,8 @@ const REMAP: Record<string, Record<string, string>> = {
};
let instanceId = "";
/** What the scenario's registry serves, by digest. */
let stocked: string[] = [];
/** The mesh's own images, as the machines hold them. */
let held: HeldImage[] = [];
function quote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
@@ -187,30 +188,15 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
return must("anchor", `docker exec mesh-control /mesh-control ${command}`, timeoutMs);
}
/** The repository path a reference serves under — registry.ts's repositoryFor, mirrored. */
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;
}
/** The pinned reference this scenario's registry serves for a repository. */
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;
/** 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);
}
/** The substrate bundle, its image references pointed at this scenario's own registry. */
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;
/** The substrate bundle: ours by the ID the machine holds, everything else upstream. */
function bundleFor(images: HeldImage[]): string {
return substrateBundle(bundle, images);
}
/**
@@ -226,7 +212,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);
@@ -282,7 +268,7 @@ before(async () => {
onProgress: (m) => console.log(`raise: ${m}`),
});
instanceId = raised.instanceId;
stocked = raised.images;
held = raised.images;
// anchor raises the substrate from its bundle, digests rewritten to the scenario registry's.
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
+101 -38
View File
@@ -1,70 +1,133 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { pinnedInto, repositoryOf, stillUnpinned } from "../src/pinning.ts";
import {
isMeshBuilt, pinnedInto, referenceFor, repositoryOf, stillUnpinned, type HeldImage,
} from "../src/pinning.ts";
const SERVED = [
"192.0.2.250:5000/postgres@sha256:" + "a".repeat(64),
"192.0.2.250:5000/mesh-provision-postgres@sha256:" + "b".repeat(64),
"192.0.2.250:5000/gitea/gitea@sha256:" + "c".repeat(64),
"192.0.2.250:5000/ghcr.io/mailu/admin@sha256:" + "d".repeat(64),
/**
* What a machine holds, and what it does not.
*
* The lab used to raise a registry inside the scenario and rewrite EVERY reference to it —
* third-party ones included. That registry exists in no production mesh, so what these tests
* describe now is the real division: our images are handed over and named by their own ID,
* everything else is pulled from the internet and left exactly as written.
*/
const HELD: HeldImage[] = [
{
requested: "mesh-control:development",
repository: "mesh-control",
reference: "sha256:" + "a".repeat(64),
},
{
requested: "mesh-runtime-postgres:development",
repository: "mesh-runtime-postgres",
reference: "sha256:" + "b".repeat(64),
},
{
requested: "mesh-route-proxy:development",
repository: "mesh-route-proxy",
reference: "sha256:" + "c".repeat(64),
},
];
test("the repository is what survives being served somewhere else", () => {
assert.equal(repositoryOf(SERVED[0]!), "postgres");
assert.equal(repositoryOf(SERVED[2]!), "gitea/gitea");
assert.equal(repositoryOf(SERVED[3]!), "ghcr.io/mailu/admin");
test("the repository is the reference without its tag", () => {
assert.equal(repositoryOf("mesh-runtime-postgres:development"), "mesh-runtime-postgres");
assert.equal(repositoryOf("alpine"), "alpine");
assert.equal(repositoryOf("gitea/gitea:1.22"), "gitea/gitea");
assert.equal(repositoryOf("ghcr.io/mailu/admin@sha256:" + "d".repeat(64)), "ghcr.io/mailu/admin");
// A port in a hostname is a colon that is NOT a tag, and treating it as one would truncate the
// host rather than the tag.
assert.equal(
repositoryOf("registry.example:5000/novox/www:latest"), "registry.example:5000/novox/www");
});
/**
* The line the whole change turns on.
*
* An image with somewhere to be fetched from is fetched from there. An image with nowhere — no
* registry host, no upstream organisation, and a `mesh-` name — is one built here and handed over.
*/
test("only what is built here and published nowhere counts as ours", () => {
for (const ours of [
"mesh-control:development", "mesh-runtime-plex:development", "mesh-route-proxy:development",
"mesh-provision-postgres@sha256:" + "0".repeat(64),
]) {
assert.ok(isMeshBuilt(ours), `${ours} is one of ours and was not recognised`);
}
for (const theirs of [
"postgres:17-alpine", "gitea/gitea:1.22", "ghcr.io/mailu/admin:1.9",
"registry.example:5000/novox/www:latest",
// A registry host in front of one of our names does NOT make it ours: it says somebody
// published it, so the machine can fetch it from there like anything else.
"registry.example:5000/mesh-control:development",
]) {
assert.ok(!isMeshBuilt(theirs), `${theirs} is not ours and was claimed`);
}
});
// The case this exists for: an image the mesh builds has no digest until it is built, so a
// manifest ships sixty-four zeros and would stop on the machine (novox/hq 04-ISSUES/025).
test("a placeholder for one of our own images becomes the one this scenario serves", () => {
const before = `"image": "mesh-provision-postgres@sha256:${"0".repeat(64)}"`;
const after = pinnedInto(before, SERVED);
assert.match(after, /192\.0\.2\.250:5000\/mesh-provision-postgres@sha256:b{64}/);
test("a placeholder for one of our own images becomes the image the machine holds", () => {
const before = `"image": "mesh-runtime-postgres@sha256:${"0".repeat(64)}"`;
const after = pinnedInto(before, HELD);
assert.equal(after, `"image": "sha256:${"b".repeat(64)}"`);
assert.deepEqual(stillUnpinned(after), []);
});
// And a real third-party digest is replaced too — the text says which image, the scenario says
// which copy of it.
test("a real digest is redirected to this scenario's copy", () => {
const before = `"image": "gitea/gitea@sha256:${"f".repeat(64)}"`;
assert.match(pinnedInto(before, SERVED), /192\.0\.2\.250:5000\/gitea\/gitea@sha256:c{64}/);
/**
* **The heart of it.** A third-party reference is not touched.
*
* It used to be rewritten to whatever the lab's registry assigned, which meant the bed never once
* fetched an image the way a real machine does — and every bootstrap fault that depended on that
* went unfound.
*/
test("a third-party image is left exactly as the manifest wrote it", () => {
for (const reference of [
`postgres@sha256:${"7".repeat(64)}`,
`gitea/gitea@sha256:${"8".repeat(64)}`,
`ghcr.io/mailu/admin@sha256:${"9".repeat(64)}`,
`registry.example:5000/novox/www:latest`,
]) {
const before = `"image": "${reference}"`;
assert.equal(pinnedInto(before, HELD), before, `${reference} was rewritten`);
}
});
test("a reference that already carries a registry is still redirected", () => {
const before = `"image": "docker.io/postgres@sha256:${"e".repeat(64)}"`;
assert.match(pinnedInto(before, SERVED), /192\.0\.2\.250:5000\/postgres@sha256:a{64}/);
});
// **Left alone, not blanked.** A repository this scenario did not stock may be reachable some
// other way, and emptying the reference would produce the exact failure this prevents.
test("something the scenario does not serve is untouched", () => {
const before = `"image": "redis@sha256:${"9".repeat(64)}"`;
assert.equal(pinnedInto(before, SERVED), before);
test("a reference of ours that already carries a registry is still redirected", () => {
// What the committed substrate bundle looks like: written for a target that had a registry.
const before = `"image": "192.0.2.250:5000/mesh-control@sha256:${"e".repeat(64)}"`;
assert.equal(pinnedInto(before, HELD), `"image": "sha256:${"a".repeat(64)}"`);
});
// A longer repository ending in a shorter one must not be half-replaced.
test("a repository that ends in another one is not partly rewritten", () => {
const before = `"image": "my-postgres@sha256:${"7".repeat(64)}"`;
assert.equal(pinnedInto(before, SERVED), before,
"'my-postgres' was rewritten because it ends in 'postgres'");
const before = `"image": "our-mesh-control@sha256:${"7".repeat(64)}"`;
assert.equal(pinnedInto(before, HELD), before,
"'our-mesh-control' was rewritten because it ends in 'mesh-control'");
});
test("every image in a whole manifest is redirected at once", () => {
test("every image in a whole manifest is settled at once", () => {
const manifest = JSON.stringify({
resources: [
{ id: "db", image: `postgres@sha256:${"1".repeat(64)}` },
{ id: "prov", image: `mesh-provision-postgres@sha256:${"0".repeat(64)}` },
{ id: "runtime", image: `mesh-runtime-postgres@sha256:${"0".repeat(64)}` },
{ id: "proxy", image: `mesh-route-proxy@sha256:${"0".repeat(64)}` },
{ id: "app", image: `gitea/gitea@sha256:${"2".repeat(64)}` },
],
});
const after = pinnedInto(manifest, SERVED);
const after = pinnedInto(manifest, HELD);
assert.deepEqual(stillUnpinned(after), []);
for (const want of ["a".repeat(64), "b".repeat(64), "c".repeat(64)]) {
assert.ok(after.includes(want), `missing ${want.slice(0, 6)}… in ${after}`);
}
assert.ok(after.includes(`sha256:${"b".repeat(64)}`), after);
assert.ok(after.includes(`sha256:${"c".repeat(64)}`), after);
// And the two that are not ours are still whole, digest and all.
assert.ok(after.includes(`postgres@sha256:${"1".repeat(64)}`), after);
assert.ok(after.includes(`gitea/gitea@sha256:${"2".repeat(64)}`), after);
});
test("what a repository is held under can be asked for, and absence is not an empty string", () => {
assert.equal(referenceFor(HELD, "mesh-control"), `sha256:${"a".repeat(64)}`);
assert.equal(referenceFor(HELD, "mesh-runtime-plex"), undefined);
});
test("what is still a placeholder can be named", () => {
+72 -1
View File
@@ -1,7 +1,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseScenario } from "../src/declaration/parse.ts";
import { planPlacements, PLACEABLE, isPlaceable } from "../src/lifecycle/place.ts";
import { planPlacements, planHeldImages, PLACEABLE, isPlaceable } from "../src/lifecycle/place.ts";
import { assertSupported, UnsupportedError } from "../src/lifecycle/supported.ts";
/**
@@ -59,6 +59,77 @@ test("placing the host is supported", () => {
assert.doesNotThrow(() => assertSupported(scenario("place:\n all: [host]")));
});
/**
* Which machine is handed which of the mesh's own images.
*
* **Not an economy — a fact.** An operator's workstation holds the images its own modules need,
* because somebody put them there, and a home server holds a different set. The lab used to serve
* everything to everyone from a registry it raised, which hid that entirely; handing every machine
* the union instead would put some thirty gigabytes of runtimes onto whole-mesh-full's
* thirty-gigabyte workstations, and the raise would die on disk with the topology looking fine.
*/
test("a machine that says nothing is handed everything the scenario has", () => {
const s = parseScenario(`
scenario: s
segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true }
images: [mesh-control:development, mesh-runtime-redis:development]
place: { all: [host, runtime] }
`);
assert.deepEqual(planHeldImages(s), [
{ machine: "anchor", images: ["mesh-control:development", "mesh-runtime-redis:development"] },
]);
});
test("a machine that names some is handed those, and no others", () => {
const s = parseScenario(`
scenario: s
segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines:
anchor:
at: { segment: hosting, address: [192.0.2.10] }
egress: true
images: [mesh-control:development]
laptop:
at: { segment: hosting, address: [192.0.2.20] }
egress: true
images: [mesh-runtime-redis:development]
images: [mesh-control:development, mesh-runtime-redis:development]
place: { all: [host, runtime] }
`);
assert.deepEqual(planHeldImages(s), [
{ machine: "anchor", images: ["mesh-control:development"] },
{ machine: "laptop", images: ["mesh-runtime-redis:development"] },
]);
});
test("a machine that names none is handed none, and is not a machine to visit", () => {
// Absent and empty are different, and a machine running nothing of ours should be able to say
// so without the lab deciding it must have meant everything.
const s = parseScenario(`
scenario: s
segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] }, egress: true }
bare: { at: { segment: hosting, address: [192.0.2.20] }, egress: true, images: [] }
images: [mesh-control:development]
place: { all: [host, runtime] }
`);
assert.deepEqual(planHeldImages(s).map((p) => p.machine), ["anchor"]);
});
test("a scenario with none of our images loads nothing anywhere", () => {
const s = parseScenario(`
scenario: s
segments: { hosting: { kind: public, cidr: [192.0.2.0/24] } }
machines:
anchor: { at: { segment: hosting, address: [192.0.2.10] } }
place: { all: [host] }
`);
assert.deepEqual(planHeldImages(s), []);
});
test("a tier that does not exist is refused BY NAME", () => {
// Named individually rather than refused as a whole, so a scenario placing a host and a
// substrate is told exactly which half the lab cannot do — rather than being told `place:`
-66
View File
@@ -1,66 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { digestFrom, pinnedReference, registryAddress, repositoryFor } from "../src/lifecycle/registry.ts";
/**
* The registry inside a scenario (novox/hq 04-ISSUES/009).
*
* These test the pure parts. The parts that need a registry are exercised by raising a
* scenario, because a fake registry would assert that the fake behaves as expected
* (novox/hq ADR 0017).
*/
test("a digest is read from what the registry actually said", () => {
// The real shape of `docker push` output. The digest here is the REGISTRY's, not Docker
// Hub's, and that is the point: a declaration pins what this registry serves.
const output =
"The push refers to repository [localhost:5000/alpine]\n" +
"63f227048c13: Pushed\n" +
"3.20: digest: sha256:6c2a9711b0a9f32b0239d9222eb1072309cf46c6431d319ae249186d811a987c size: 528\n";
assert.equal(
digestFrom(output),
"sha256:6c2a9711b0a9f32b0239d9222eb1072309cf46c6431d319ae249186d811a987c",
);
});
test("no digest is not an empty digest", () => {
// A push that reported no digest leaves nothing for a declaration to pin, and inventing one
// would be worse than failing — the host would refuse it later, further from the cause.
assert.equal(digestFrom("The push refers to repository [localhost:5000/alpine]\n"), null);
assert.equal(digestFrom(""), null);
// Hex, but the wrong LENGTH. An earlier version used "tooshort", whose letters fall outside
// a-f — so it failed the character class and proved nothing about the length check.
assert.equal(digestFrom("digest: sha256:abc123"), null);
assert.equal(digestFrom("digest: sha256:" + "a".repeat(63)), null, "63 is not 64");
});
test("the repository is the reference without its tag", () => {
assert.equal(repositoryFor("alpine:3.20"), "alpine");
assert.equal(repositoryFor("alpine"), "alpine");
assert.equal(repositoryFor("library/postgres:17"), "library/postgres");
// A port in a hostname is a colon that is NOT a tag, and treating it as one would serve the
// image from a truncated path.
assert.equal(repositoryFor("localhost:5000/alpine:3.20"), "localhost:5000/alpine");
assert.equal(repositoryFor("localhost:5000/alpine"), "localhost:5000/alpine");
});
test("the registry's address is derived from its segment", () => {
assert.equal(registryAddress("192.0.2.0/24"), "192.0.2.250");
assert.equal(registryAddress("198.51.100.0/24"), "198.51.100.250");
// An IPv6-only segment cannot host it, and saying so beats producing an address nothing
// can be pointed at.
assert.throws(() => registryAddress("2001:db8:a::/48"), /not an IPv4 network/);
});
test("what a declaration pins is the registry's own digest", () => {
// Not Docker Hub's. ADR 0006 requires a reference that is exact and cannot move, and a
// digest this registry assigned is both.
const pinned = pinnedReference("192.0.2.250", {
requested: "alpine:3.20",
repository: "alpine",
digest: "sha256:" + "6".repeat(64),
});
assert.equal(pinned, `192.0.2.250:5000/alpine@sha256:${"6".repeat(64)}`);
assert.ok(pinned.includes("@sha256:"), "the host refuses anything not pinned by digest");
assert.ok(!pinned.includes(":3.20"), "a tag would move; the digest is what is pinned");
});
+59 -2
View File
@@ -1,5 +1,6 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readdirSync } from "node:fs";
import { parseScenario } from "../src/declaration/parse.ts";
import { loadScenario } from "../src/declaration/parse.ts";
import { planRouters } from "../src/lifecycle/router.ts";
@@ -13,8 +14,13 @@ function refuses(yaml: string, pattern: RegExp): void {
}
test("the shipped scenarios are valid", () => {
for (const file of ["scenarios/bootstrap-single.yml", "scenarios/the-ordinary-shape.yml"]) {
assert.doesNotThrow(() => loadScenario(file));
// **Every one of them**, not a chosen two. Thirty-odd scenarios were rewritten in one pass when
// the lab's registry was removed, and a scenario nobody loads is a scenario nobody validates —
// which is how a bed goes unraisable for weeks and is only found when somebody wants it.
const files = readdirSync("scenarios").filter((f) => f.endsWith(".yml"));
assert.ok(files.length > 20, `only ${files.length} scenarios found — is the path right?`);
for (const file of files) {
assert.doesNotThrow(() => loadScenario(`scenarios/${file}`), `scenarios/${file}`);
}
});
@@ -251,6 +257,57 @@ machines: { a: { at: detached, egress: true } }`,
/detached but declares egress/);
});
/**
* `images:` is the mesh's own images and nothing else.
*
* **The rule that replaced the lab's registry.** Anything with somewhere to be fetched from is
* fetched from there, by the machine, over its uplink. Serving it from inside the scenario instead
* is what hid the bootstrap faults this lab exists to find — so it is refused rather than quietly
* done, or the fiction comes back one convenient line at a time.
*/
test("a third-party image in images: is refused, because nothing loads it", () => {
for (const image of ["postgres:17-alpine", "gitea/gitea:1.22", "ghcr.io/mailu/admin:1.9"]) {
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: ["${image}"]
place: { all: [runtime] }`,
/is not one of the mesh's own images/);
}
});
test("one of ours in images: is accepted", () => {
assert.doesNotThrow(() => parseScenario(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: [mesh-control:development, mesh-route-proxy:development]
place: { all: [runtime] }`));
});
test("images: is named by tag — an image ID is not knowable until the image is built", () => {
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
images: ["mesh-control@sha256:${"0".repeat(64)}"]
place: { all: [runtime] }`,
/is pinned by digest/);
});
test("a machine cannot be handed an image the scenario does not have", () => {
// Ignoring it silently would be a machine missing a runtime, failing several minutes later
// inside an apply, as a container that will not start.
refuses(`scenario: x
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
machines:
a:
at: { segment: net, address: [192.0.2.1] }
egress: true
images: [mesh-runtime-redis:development]
images: [mesh-control:development]
place: { all: [runtime] }`,
/is not in this scenario's images/);
});
test("a segment may not be named 'uplink' — the lab claims that name for egress", () => {
refuses(`scenario: x
segments: { uplink: { kind: public, cidr: [192.0.2.0/24] } }