Delete the lab's registry, and bootstrap the anchor through the installer #19

Merged
jschoubben merged 14 commits from feat/bed-bootstraps-through-the-installer into main 2026-09-11 19:57:05 +00:00
5 changed files with 75 additions and 7 deletions
Showing only changes of commit 6c09ddb528 - Show all commits
+2 -2
View File
@@ -15,7 +15,7 @@
import type { Scenario, Segment } from "./types.ts";
import { contains, familyOf, parseAddress, parseCidr, type Cidr } from "./net.ts";
import { isMeshBuilt } from "../pinning.ts";
import { mustBeHandedOver } from "../pinning.ts";
/** RFC 5737 and RFC 3849. The only addresses guaranteed never to route on the real internet. */
const DOCUMENTATION_RANGES = [
@@ -327,7 +327,7 @@ export function validate(scenario: Scenario): void {
`images: '${image}' is pinned by digest. Name it by tag — what a declaration uses is the ` +
`ID of the image loaded onto the machine, reported when the scenario is raised`,
);
} else if (!isMeshBuilt(image)) {
} else if (!mustBeHandedOver(image)) {
// **The rule that replaced the lab's registry.** Anything with somewhere to be fetched from
// is fetched from there, over the machine's uplink, exactly as in production. Serving it
// from inside the scenario instead is what hid the bootstrap faults this lab exists to find.
+2 -2
View File
@@ -19,7 +19,7 @@ import { join } from "node:path";
import { incus, incusOk, succeeds } from "../incus/client.ts";
import { around, log, shorten } from "../log.ts";
import { isMeshBuilt, repositoryOf, type HeldImage } from "../pinning.ts";
import { mustBeHandedOver, repositoryOf, type HeldImage } from "../pinning.ts";
/**
* What this stage can put inside a machine.
@@ -448,7 +448,7 @@ export async function loadHeldImages(
// Refused by the validator, so reaching here would be a validator bug — but the consequence
// is a third-party image quietly loaded from the workstation instead of pulled, which is the
// fiction all of this exists to remove. Cheap to check, expensive to miss.
if (!isMeshBuilt(requested)) {
if (!mustBeHandedOver(requested)) {
throw new Error(
`images: '${requested}' is not one of the mesh's own images. It is pulled from the ` +
`internet by the machine that needs it, not loaded from this workstation.`,
+40
View File
@@ -67,6 +67,46 @@ export function isMeshBuilt(reference: string): boolean {
return !repository.includes("/") && repository.startsWith("mesh-");
}
/**
* Registries an anonymous pull works against.
*
* Not a list of what is trusted — a list of where no account is needed. Everything else wants one,
* and a scenario machine has none.
*/
const PUBLIC_REGISTRIES = [
"docker.io", "ghcr.io", "quay.io", "lscr.io", "gcr.io", "registry.k8s.io",
"public.ecr.aws", "mcr.microsoft.com", "docker.elastic.co", "registry.gitlab.com",
];
/** The registry a reference names, or "" when it names none and so means Docker Hub. */
export function registryOf(reference: string): string {
const first = repositoryOf(reference).split("/")[0] ?? "";
// A first segment is a registry only if it looks like a host: `novox/www` is an organisation on
// Docker Hub; `registry.example/novox/www` is somewhere else entirely.
return first.includes(".") || first.includes(":") ? first : "";
}
/**
* Whether the workstation has to hand this image over rather than let the machine fetch it.
*
* **Two reasons, one consequence.** An image the mesh builds for itself exists in no registry at
* all. An image in the operator's *private* registry exists in one the machines have no account
* for, and the pull fails with `no basic auth credentials` — which is not something more patience
* fixes. Either way the machine cannot get it alone, so the workstation, which does hold the
* credential, exports it and loads it.
*
* **This stands in for something, and it is worth saying what.** In a finished mesh these are built
* by the mesh's builder and published to the mesh's own store, and every machine pulls them from
* there with a credential the mesh granted it. Until that store exists there is nowhere for them to
* come from — and handing them over is the closest honest thing to it, rather than a registry the
* lab invents, which is exactly what was just removed.
*/
export function mustBeHandedOver(reference: string): boolean {
if (isMeshBuilt(reference)) return true;
const registry = registryOf(reference);
return registry !== "" && !PUBLIC_REGISTRIES.includes(registry);
}
/**
* Replace every reference to one of the mesh's own images with the image the machine holds.
*
+2 -2
View File
@@ -15,7 +15,7 @@ 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";
import { mustBeHandedOver, pinnedInto, referenceFor, repositoryOf, type HeldImage } from "../../src/pinning.ts";
// --- the substrate bundle, and what its three images are on a real machine ---------------------
@@ -107,7 +107,7 @@ const UPSTREAM = new Map<string, string>([
* rather than an assertion here taking the whole bed down before it starts.
*/
export function onTheMachine(reference: string, held: HeldImage[]): string {
if (isMeshBuilt(reference)) {
if (mustBeHandedOver(reference)) {
const found = referenceFor(held, repositoryOf(reference));
assert.ok(
found,
+29 -1
View File
@@ -2,7 +2,8 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import {
isMeshBuilt, pinnedInto, referenceFor, repositoryOf, stillUnpinned, type HeldImage,
isMeshBuilt, mustBeHandedOver, pinnedInto, referenceFor, repositoryOf, stillUnpinned,
type HeldImage,
} from "../src/pinning.ts";
/**
@@ -134,3 +135,30 @@ test("what is still a placeholder can be named", () => {
const text = `"image": "something-of-ours@sha256:${"0".repeat(64)}"`;
assert.deepEqual(stillUnpinned(text), ["something-of-ours"]);
});
/**
* An image a machine cannot fetch by itself has to be handed to it, and there are two ways to be in
* that position: built here and published nowhere, or sitting in a registry the machine has no
* account for. The second was found by deleting the lab's registry — the operator's own images
* failed with `no basic auth credentials`, which no amount of retrying improves.
*/
test("an image the machine cannot fetch by itself is handed over", () => {
for (const handed of [
"mesh-control:development",
"mesh-runtime-plex:development",
`registry.example/novox/www@sha256:${"a".repeat(64)}`,
"registry.example:5000/novox/photos-server:latest",
]) {
assert.ok(mustBeHandedOver(handed), `${handed} cannot be fetched and was not handed over`);
}
for (const fetched of [
"postgres:17-alpine",
"gitea/gitea:1.22",
"ghcr.io/mailu/admin:1.9",
"quay.io/keycloak/keycloak:26",
"lscr.io/linuxserver/sonarr:latest",
"mcr.microsoft.com/mssql/server:2022-latest",
]) {
assert.ok(!mustBeHandedOver(fetched), `${fetched} can be fetched and was handed over anyway`);
}
});