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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user