Files
mesh-lab/src/pinning.ts
T
jschoubben 5d6e8fbe7a Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:40:40 +02:00

143 lines
7.2 KiB
TypeScript

/**
* Naming the mesh's own images by what they are.
*
* **A digest is not knowable until something is built** (novox/hq 04-ISSUES/025). A manifest in a
* repository can pin a third-party image, because somebody can ask a registry what a tag points
* at. It cannot pin an image the mesh builds itself: mesh-controller, mesh-builder, mesh-route-proxy,
* the per-module runtimes and the provisioners exist in no registry, so there is no manifest
* digest to write down. The catalogue ships sixty-four zeros for them, which parses, resolves,
* composes — and stops on the machine.
*
* The lab used to answer that with a registry of its own: raise one inside the scenario, push
* everything into it, and rewrite every reference — third-party ones included — to the digest it
* assigned. **That registry does not exist in production, so the lab was testing a fiction**, and
* the fiction hid the bootstrap problems it was supposed to find.
*
* What is true instead is two things:
*
* - **Third-party images are pulled from the internet.** They are left exactly as written, and
* the machine fetches them over its `egress` uplink the way any machine does.
* - **The mesh's own images are built and handed over.** They are loaded onto the machine from
* the workstation that built them, and named by the digest of their own image configuration —
* a bare `sha256:…`, which mesh-host accepts as "an image this machine already holds"
* (mesh-host, *an image may be named by the digest of its own configuration*).
*
* So the rewriting that remains is only the second kind, and it matches on the **repository**,
* because that is the part a person writes and the only part a placeholder digest does not say.
*/
/** What the lab loaded onto a machine, and what a declaration should call it. */
export interface HeldImage {
/** As the scenario asked for it, a tag this workstation holds: `mesh-runtime-postgres:development`. */
requested: string;
/** What a manifest names it by, with no tag and no digest: `mesh-runtime-postgres`. */
repository: string;
/**
* The reference a declaration uses: a bare `sha256:<64 hex>`.
*
* The image's own ID — the digest of its configuration — which `docker load` preserves, so the
* name is the same on the workstation that built it and on every machine it was handed to.
*/
reference: string;
}
/** `alpine:3.20` and `alpine` both mean `alpine`; `foo/bar:1` means `foo/bar`. */
export function repositoryOf(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;
}
/**
* Whether a reference names an image the mesh builds for itself.
*
* **Derived from the shape the build produces, not from a list of names.** `make image
* builder-image provisioner-image objectstore-image redis-provisioner-image proxy-image` in
* mesh-controller and `scripts/build-module-runtime.sh` here both tag their output `mesh-<something>`
* with no registry host and no upstream organisation — that is what "built here, published
* nowhere" looks like, and a hardcoded list would go stale the first time a module is added.
*
* The absence of a slash carries the weight: `ghcr.io/mailu/admin` and
* `registry.example/novox/www` both say where they are fetched from, and a bare
* `mesh-runtime-plex` says there is nowhere.
*/
export function isMeshBuilt(reference: string): boolean {
const repository = repositoryOf(reference);
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.
*
* Matching is on the repository and ignores whatever digest was written down — a manifest says
* `mesh-runtime-postgres@sha256:0000…` and means *the runtime this machine was given*.
*
* **Everything else is left exactly as it is.** `postgres@sha256:7456…`, `gitea/gitea@sha256:…`
* and `ghcr.io/mailu/admin@sha256:…` are pulled from the internet over the machine's uplink, which
* is what a real machine does and the reason the lab's own registry is gone.
*/
export function pinnedInto(text: string, held: HeldImage[]): string {
let out = text;
for (const image of held) {
const escaped = image.repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Optionally a registry, then the repository, then any digest. Anchored on a quote or
// whitespace so a longer repository ending in a shorter one is not half-replaced.
out = out.replaceAll(
new RegExp(`(?<=^|["\\s])(?:[A-Za-z0-9_.:-]+\\/)*${escaped}@sha256:[0-9a-f]{64}`, "g"),
image.reference,
);
}
return out;
}
/** The reference for one repository, or undefined if this scenario loaded no such image. */
export function referenceFor(held: HeldImage[], repository: string): string | undefined {
return held.find((image) => image.repository === repository)?.reference;
}
/** Whether anything is still pinned to a placeholder, which would fail on the machine. */
export function stillUnpinned(text: string): string[] {
return [...text.matchAll(/([A-Za-z0-9_.:/-]+)@sha256:0{64}/g)].map((m) => m[1]!);
}