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