Delete the lab's registry, and bootstrap the anchor through the installer #19
@@ -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.
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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`);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user