/** * 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-` * 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]!); }