From 7e3481f025d5533e0a30a7cdf8afbbc0a240c494 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 11 Sep 2026 11:38:15 +0200 Subject: [PATCH] bootstrap: the slot the installer fills is not a registry to reach for The first real run of mesh-bootstrap stopped in preflight, dialling 192.0.2.250:5000 for ninety seconds on a machine whose network was fine. That address is the registry the lab used to raise; the substrate template still names the control plane by it, and step 3 replaces that reference with the id of the image this installer carries. Nothing ever pulls it. So preflight excludes the control plane's resource by identity, rather than by the happy accident of the template filling its slot with something that needs no registry. Every other container's registry is still dialled, because those are somebody else's images at somebody else's registry and a machine that cannot reach one fails inside a pull, which says the wrong thing. Also: `make bootstrap` takes BOOTSTRAP_OUT. The lab now builds the installer from source before every raise, into a path it chooses, and a caller that could not say where the output goes would have to copy it afterwards. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- Makefile | 12 ++++++++++-- internal/bootstrap/preflight.go | 16 +++++++++++++++- internal/bootstrap/preflight_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1c5f2c2..f65db19 100644 --- a/Makefile +++ b/Makefile @@ -75,6 +75,14 @@ host: # hold, and the tag is the only name that survives the transfer. Saving by id produces an archive # with no tags at all, which the installer refuses; caught here instead, in front of the person who # can fix it. +# +# BOOTSTRAP_OUT is where the binary is written, and it exists because something other than a person +# now builds this: the lab rebuilds every artifact it runs from source before a raise, into paths it +# chose (mesh-lab's src/rebuild.ts, and novox/hq 04-ISSUES/005 for why it does that at all). A +# caller that could not say where the output goes would have to copy it afterwards, which is one +# more step to forget. +BOOTSTRAP_OUT ?= mesh-bootstrap + bootstrap: @test -n "$(IMAGE)" || { echo "IMAGE= is required; an installer carrying no control-plane image cannot raise a mesh"; exit 1; } @case "$(IMAGE)" in sha256:*) echo "IMAGE=$(IMAGE) is an image id. The installer identifies the carried image by its tag, because an id is the digest of a configuration that a runtime rewrites as it loads. Pass a name:tag"; exit 1;; esac @@ -82,11 +90,11 @@ bootstrap: @test -n "$$(docker image inspect --format '{{len .RepoTags}}' "$(IMAGE)" | grep -v '^0$$')" || { echo "$(IMAGE) has no repository tag, so the saved archive would carry no name the installer can ask a runtime about. Tag it first: docker tag $(IMAGE) mesh-control:"; exit 1; } @cp internal/image/control-plane.tar internal/image/control-plane.tar.placeholder @docker save --output internal/image/control-plane.tar "$(IMAGE)" - @CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION)" -o mesh-bootstrap ./cmd/mesh-bootstrap; \ + @CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION)" -o "$(BOOTSTRAP_OUT)" ./cmd/mesh-bootstrap; \ status=$$?; \ mv internal/image/control-plane.tar.placeholder internal/image/control-plane.tar; \ exit $$status - @echo "built mesh-bootstrap carrying $(IMAGE)" + @echo "built $(BOOTSTRAP_OUT) carrying $(IMAGE)" clean: rm -f mesh-host mesh-bootstrap diff --git a/internal/bootstrap/preflight.go b/internal/bootstrap/preflight.go index e13e276..4c0bb21 100644 --- a/internal/bootstrap/preflight.go +++ b/internal/bootstrap/preflight.go @@ -104,6 +104,15 @@ func Preflight(ctx context.Context, o Options, d Deps, say func(string)) ([]byte // reason. Everything else is somebody else's image at somebody else's registry, and a machine // that cannot reach it fails inside a pull, which reports a network error where a person // reads a missing image. + // + // "The mesh's own image is skipped" used to mean "skipped if the template happened to name it + // in a way that needs no registry", and that is not the same sentence. A template names the + // control plane by SOMETHING — the reference is a slot, and step 3 replaces whatever is in it + // with the id of the image this installer carries. Whatever the slot held is therefore never + // pulled, never fetched, and never reached; requiring it to be reachable refuses a correct + // install because of a string that is about to be thrown away. Found on the first real run: the + // lab's template still carried `192.0.2.250:5000/mesh-control@…`, the address of a registry that + // no longer exists, and preflight timed out dialling it. for _, host := range registriesIn(parsed) { dialing, cancel := context.WithTimeout(ctx, o.Timeout) err := d.Dial(dialing, host) @@ -171,12 +180,17 @@ func containerRuntimeDetector(run Runner) profile.Detector { // registriesIn is every host the bundle's images would be fetched from, without duplicates and in // the order they appear. +// +// The control plane's own resource is excluded by identity rather than by the shape of what it +// names. Its image reference is a slot the installer overwrites with the id of the image it +// carries, so no registry ever serves it — and a template that filled that slot with a registry +// this machine cannot reach is not a machine with a network problem. func registriesIn(d *declaration.Declaration) []string { var hosts []string seen := map[string]bool{} for _, r := range d.Resources { container, ok := r.(*declaration.Container) - if !ok { + if !ok || container.Identity() == ControlPlaneID { continue } host, served := registryOf(container.Image) diff --git a/internal/bootstrap/preflight_test.go b/internal/bootstrap/preflight_test.go index 627dce0..1664da1 100644 --- a/internal/bootstrap/preflight_test.go +++ b/internal/bootstrap/preflight_test.go @@ -100,6 +100,31 @@ func TestOnlyTheRegistriesTheBundleNamesAreAskedAbout(t *testing.T) { } } +// And the control plane's slot is excluded whatever is in it. +// +// Found on the first real run of this installer. A template names the control plane by SOMETHING +// and step 3 replaces it with the id of the carried image, so whatever was there is never pulled — +// but preflight was reading that slot like any other and dialling it. The lab's template still +// carried the address of a registry the lab no longer raises, so a correct install timed out in +// preflight against a machine with a perfectly good network. +func TestTheControlPlanesOwnRegistryIsNeverAskedAbout(t *testing.T) { + parsed, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[ + {"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` + + strings.Repeat("7", 64) + `"}, + {"id":"control-plane","type":"container","name":"mesh-control","image":"192.0.2.250:5000/mesh-control@sha256:` + + strings.Repeat("8", 64) + `"} + ]}`)) + if err != nil { + t.Fatal(err) + } + + got := registriesIn(parsed) + if len(got) != 1 || got[0] != DefaultRegistry { + t.Fatalf("asked about %v; the control plane's own reference is about to be replaced and "+ + "must not be reached for", got) + } +} + func TestWhereAnImageWouldBeFetchedFrom(t *testing.T) { // The container runtime's own rule: the part before the first slash is a registry host if it // has a dot, a port, or is localhost. Getting this wrong means dialling a hostname that is