From 52231692265d6529fb06a19f88672bedd2591a05 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 15:17:47 +0200 Subject: [PATCH 1/3] Genesis registers the control plane with the manifest its build produced The control plane's manifest existed twice: at the root of its repository, read whenever the mesh rebuilds it from source, and as a copy in the catalogue, read by genesis. Nothing kept them equal, and the first rebuild replaced the mesh's record with the repository's shape while every later push was refused (novox/hq 04-ISSUES/072). The builder's one-shot result already carries the manifest it built, artifact resolved to the image; step 3 keeps it and step 9 registers it, re-pinning the built image's bare id to the reference the registry assigned. The catalogue is still read for the registry's and the builder's manifests and for phase two. --- cmd/mesh-bootstrap/main.go | 2 +- internal/bootstrap/bootstrap.go | 10 ++-- internal/bootstrap/build.go | 32 ++++++++-- internal/bootstrap/build_test.go | 54 +++++++++++++++++ internal/bootstrap/builder.go | 2 +- internal/bootstrap/control.go | 96 +++++++++++++----------------- internal/bootstrap/control_test.go | 79 +++++++++++++----------- internal/bootstrap/module.go | 66 ++++++++++++++++++++ 8 files changed, 242 insertions(+), 99 deletions(-) diff --git a/cmd/mesh-bootstrap/main.go b/cmd/mesh-bootstrap/main.go index 5f28e15..e48aa1a 100644 --- a/cmd/mesh-bootstrap/main.go +++ b/cmd/mesh-bootstrap/main.go @@ -221,7 +221,7 @@ func newFlagSet(opts *bootstrap.Options, jsonOut *bool) *flag.FlagSet { set.StringVar(&opts.Out, "out", opts.Out, "where the produced bundle is written") set.StringVar(&opts.State, "state", opts.State, "where this node records what it has applied") set.StringVar(&opts.Catalogue, "catalog", opts.Catalogue, - "a checkout of the mesh's catalogue; without it this stops after the foundation") + "a checkout of the mesh's catalogue, for the registry's and the builder's manifests and what phase two installs; without it this stops after the foundation") set.StringVar(&opts.Source.Repository, "source", opts.Source.Repository, "the repository the control plane is built from, on a mesh that already exists") set.StringVar(&opts.Source.Ref, "source-ref", opts.Source.Ref, diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 049112a..9d3d787 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -137,9 +137,11 @@ type Options struct { Node string // Catalogue is a checkout of the mesh's catalogue repository, which is where the registry's and - // the control plane's manifests are read from. Empty stops the installer after the foundation: - // there is no pivot without manifests, and pretending otherwise would leave a machine that - // looks installed and cannot upgrade itself. + // the builder's manifests are read from, and everything phase two installs. Not the control + // plane's: that one comes out of the build at step 3, from the root of its own repository + // (novox/hq ADR 0069). Empty stops the installer after the foundation: there is no pivot + // without manifests, and pretending otherwise would leave a machine that looks installed and + // cannot upgrade itself. Catalogue string // Source is where the control plane is built from — a repository on a mesh that already @@ -585,7 +587,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro // ---- 9. control plane ----------------------------------------------------------------- say("control plane — installed as an ordinary module, pinned to that digest") permanent, err := InstallControlPlane(ctx, o, d, temporary, rewritten.Declaration, - published.Reference, say) + built, published.Reference, say) result.Permanent, result.PermanentAnswered = permanent.Container, permanent.Answered result.StoresDelivered = permanent.Delivered if err != nil { diff --git a/internal/bootstrap/build.go b/internal/bootstrap/build.go index 12983e9..b49bda9 100644 --- a/internal/bootstrap/build.go +++ b/internal/bootstrap/build.go @@ -1,6 +1,7 @@ package bootstrap import ( + "bytes" "context" "encoding/json" "errors" @@ -57,13 +58,19 @@ type Built struct { // Image is the artifact, named by the digest of its own configuration — the identity a machine // can use with nothing serving it, and the same one the installer used for a carried image. Image string + // Manifest is the module as the mesh should hold it: the manifest at the root of the repository + // that was built, its artifact resolved to Image. It is the control plane's ONE manifest + // (novox/hq ADR 0069) — the installer used to read a second copy out of the catalogue, and the + // two drifted apart the first time somebody edited one (novox/hq 04-ISSUES/072). + Manifest []byte } // builderOutput is the part of the builder's one-shot result this needs. type builderOutput struct { - Module string `json:"module"` - Commit string `json:"commit"` - Made []struct { + Module string `json:"module"` + Commit string `json:"commit"` + Manifest json.RawMessage `json:"manifest"` + Made []struct { Name string `json:"name"` Kind string `json:"kind"` Reference string `json:"reference"` @@ -142,8 +149,25 @@ func BuildControlPlane(ctx context.Context, run Runner, builderTag string, sourc result.Module, len(images)) } + // The manifest is what the mesh will hold the control plane as, so a result without one is + // a build the installer cannot finish — refused here, beside the builder that said it, rather + // than at step 9 with a message about a missing file. + manifest := bytes.TrimSpace(result.Manifest) + if len(manifest) == 0 || bytes.Equal(manifest, []byte("null")) { + return Built{}, fmt.Errorf( + "%s built, and the builder reported no manifest for it. The installer registers the "+ + "control plane with the manifest the build produced — the one at the root of its "+ + "repository, its artifact resolved — and has no other copy to use", result.Module) + } + if !bytes.Contains(manifest, []byte(`"`+images[0]+`"`)) { + return Built{}, fmt.Errorf( + "%s built %s, and the manifest the builder reported does not name that image, so "+ + "the installer cannot tell which of its resources runs the control plane", + result.Module, images[0]) + } + say(fmt.Sprintf(" built %s from %s", result.Module, shortRef(result.Commit))) - return Built{Module: result.Module, Commit: result.Commit, Image: images[0]}, nil + return Built{Module: result.Module, Commit: result.Commit, Image: images[0], Manifest: manifest}, nil } func shortRef(ref string) string { diff --git a/internal/bootstrap/build_test.go b/internal/bootstrap/build_test.go index 9abd359..00f491f 100644 --- a/internal/bootstrap/build_test.go +++ b/internal/bootstrap/build_test.go @@ -1,6 +1,7 @@ package bootstrap import ( + "context" "strings" "testing" ) @@ -39,3 +40,56 @@ func TestARepositoryAndACommitIsEnough(t *testing.T) { t.Fatalf("a repository and a commit were refused: %v", err) } } + +// **The build hands over the manifest, and the installer registers that one.** The control plane +// used to have two manifests — one at the root of its repository, which the mesh reads whenever +// it rebuilds the control plane from source, and a copy in the catalogue, which genesis read — and +// nothing kept them equal (novox/hq 04-ISSUES/072). The builder already reports the manifest it +// built, artifact resolved to the image; genesis takes it from there and reads no second copy. +func TestTheBuildHandsOverTheManifestTheMeshWillHold(t *testing.T) { + manifest := `{"module":"mesh-controller","version":"1","resources":[` + + `{"id":"server","type":"container","name":"mesh-controller","image":"` + builtImage + `"}]}` + runtime := &asked{answer: func(string, []string) (string, error) { + return `{"module":"mesh-controller","commit":"a1b2c3d4","manifest":` + manifest + + `,"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}` + "\n", nil + }} + built, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test", + Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {}) + if err != nil { + t.Fatal(err) + } + if built.Image != builtImage { + t.Errorf("the built image is %q", built.Image) + } + if string(built.Manifest) != manifest { + t.Errorf("the manifest handed over is not the one the builder reported:\n%s", built.Manifest) + } +} + +// A result without a manifest is a build the installer cannot finish, and it is refused beside the +// builder that said it rather than at step 9 with a message about a missing file. +func TestABuildReportingNoManifestIsRefused(t *testing.T) { + runtime := &asked{answer: func(string, []string) (string, error) { + return `{"module":"mesh-controller","commit":"a1b2c3d4",` + + `"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}`, nil + }} + _, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test", + Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {}) + if err == nil || !strings.Contains(err.Error(), "no manifest") { + t.Fatalf("a build reporting no manifest was accepted, or refused for another reason: %v", err) + } +} + +// And a manifest that does not name the image the build produced describes some other build. +func TestABuildWhoseManifestNamesAnotherImageIsRefused(t *testing.T) { + runtime := &asked{answer: func(string, []string) (string, error) { + return `{"module":"mesh-controller","commit":"a1b2c3d4","manifest":{"module":"mesh-controller",` + + `"resources":[{"id":"server","type":"container","image":"sha256:` + strings.Repeat("9", 64) + `"}]},` + + `"made":[{"name":"server","kind":"image","reference":"` + builtImage + `"}]}`, nil + }} + _, err := BuildControlPlane(context.Background(), runtime.run, "mesh-builder:test", + Source{Repository: "https://example.invalid/mesh-controller.git", Ref: "a1b2c3d4"}, false, func(string) {}) + if err == nil || !strings.Contains(err.Error(), "does not name that image") { + t.Fatalf("a manifest naming another image was accepted, or refused for another reason: %v", err) + } +} diff --git a/internal/bootstrap/builder.go b/internal/bootstrap/builder.go index 1b3d94f..b86c1b1 100644 --- a/internal/bootstrap/builder.go +++ b/internal/bootstrap/builder.go @@ -53,7 +53,7 @@ func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane "This is the manifest that makes the builder an ordinary module. Without it the mesh "+ "has the image and no way to run it, so nothing can be built here", err) } - pinned, places, err := pinImage(manifest, published.Reference, BuilderModule) + pinned, places, err := pinPlaceholder(manifest, published.Reference, BuilderModule) if err != nil { return out, err } diff --git a/internal/bootstrap/control.go b/internal/bootstrap/control.go index e484d19..1af4bce 100644 --- a/internal/bootstrap/control.go +++ b/internal/bootstrap/control.go @@ -53,26 +53,33 @@ type Permanent struct { // exactly the path for a value the mesh must carry and could not have invented — and they are read // out of the bundle this installer produced rather than reconstructed, because the bundle is what // created them and a second opinion about what a DSN should say is a second chance to be wrong. +// +// **The manifest is the one the build produced** — the manifest at the root of the control plane's +// own repository, its artifact resolved to the image built at step 3 (novox/hq ADR 0069). The +// installer used to read a second copy out of the catalogue and pin its placeholder; the copies +// drifted, and the first rebuild from source replaced the mesh's record with the repository's shape +// while every later push was refused on its behalf (novox/hq 04-ISSUES/072). There is one manifest +// now, and the only thing this step changes in it is the image's name: the id the machine built it +// under becomes the reference the registry assigned at step 8. func InstallControlPlane(ctx context.Context, o Options, d Deps, control controlPlane, - foundation *declaration.Declaration, image string, say func(string)) (Permanent, error) { + foundation *declaration.Declaration, built Built, image string, say func(string)) (Permanent, error) { out := Permanent{Image: image} - manifest, err := readManifest(o.Catalogue, ControlPlaneModule) - if err != nil { + if len(built.Manifest) == 0 { return out, fmt.Errorf( - "%w\n"+ - "This is the manifest that makes the control plane an ordinary module. Without it "+ - "the machine keeps the temporary control plane the foundation raised, which works "+ - "and cannot be upgraded — so the install stops here rather than pretending to "+ - "have pivoted", err) + "the control plane was not built, so there is no manifest to register it with. " + + "This is the manifest that makes the control plane an ordinary module. Without it " + + "the machine keeps the temporary control plane the foundation raised, which works " + + "and cannot be upgraded — so the install stops here rather than pretending to " + + "have pivoted") } - pinned, places, err := pinImage(manifest, image, ControlPlaneModule) + pinned, places, err := pinImage(built.Manifest, built.Image, image, ControlPlaneModule) if err != nil { return out, err } - say(fmt.Sprintf(" pinned %s, in %d place(s)", image, places)) + say(fmt.Sprintf(" pinned %s → %s, in %d place(s)", shortRef(built.Image), image, places)) container, _, err := containerIn(pinned, controlPlaneResourceIn(pinned)) if err != nil { @@ -118,52 +125,34 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control return out, nil } -// pinImage replaces the catalogue's placeholder digest with what the registry assigned. +// pinImage replaces the image the build named with the reference the registry assigned. // // **Textual, and every place it appears.** A manifest may name its image in more than one resource -// — the catalogue's converted modules routinely carry a runtime container beside the application's -// — and the same reasoning as the bundle rewrite applies: replacing one and not the others leaves -// something pointing at an image nothing serves, and it fails half way through an apply rather -// than here. +// — a migrate step beside the server, a runtime beside the application — and the same reasoning +// as the bundle rewrite applies: replacing one and not the others leaves something pointing at an +// image nothing serves, and it fails half way through an apply rather than here. // -// It refuses a manifest with no placeholder in it. That is not pedantry: a manifest already -// carrying a real digest is one somebody pinned by hand, and quietly registering it would install a -// control plane that is not the image this machine just published — which is the one thing this -// step exists to guarantee. -func pinImage(manifest []byte, reference, module string) ([]byte, int, error) { - places := bytes.Count(manifest, []byte(placeholderDigest)) +// The built image is named by the digest of its own configuration, `sha256:…` with no registry in +// front, which only the machine that built it can resolve. The whole JSON string moves to the +// registry's `/@sha256:…`, so every machine the module is later pushed to +// pulls it from the mesh's own store. +// +// It refuses a manifest that does not name the built image. That is not pedantry: a manifest +// naming some other image is one that describes some other build, and quietly registering it would +// install a control plane that is not the image this machine just published — which is the one +// thing this step exists to guarantee. +func pinImage(manifest []byte, built, reference, module string) ([]byte, int, error) { + from := []byte(`"` + built + `"`) + places := bytes.Count(manifest, from) if places == 0 { return nil, 0, fmt.Errorf( - "the %s module's manifest carries no placeholder digest (%s), so there is nothing to "+ - "pin to the image this machine just published.\n"+ - "A manifest already naming a digest was pinned by somebody else, to some other "+ - "build. Registering it would install a module that is not the one this "+ - "installer carried and pushed", module, placeholderDigest) + "the %s module's manifest does not name the image this machine built (%s), so there "+ + "is nothing to pin to the image it just published.\n"+ + "A manifest naming some other image describes some other build. Registering it "+ + "would install a module that is not the one this installer built and pushed", + module, built) } - // The reference the registry gave back is `/@sha256:…`, and what the - // manifest holds is `@sha256:0…0`. Replacing only the digest would leave the - // manifest's own repository name in front of it — which may be `mesh-controller` with no - // registry, and a runtime would then pull it from the internet. The whole reference moves. - var out bytes.Buffer - rest := manifest - for { - at := bytes.Index(rest, []byte(placeholderDigest)) - if at < 0 { - out.Write(rest) - break - } - // Back up over the repository this digest belongs to, which runs to the opening quote. - start := bytes.LastIndexByte(rest[:at], '"') - if start < 0 { - return nil, 0, fmt.Errorf( - "the %s module's manifest has a placeholder digest that is not inside a JSON "+ - "string, so the installer cannot tell what image it belongs to", module) - } - out.Write(rest[:start+1]) - out.WriteString(reference) - rest = rest[at+len(placeholderDigest):] - } - pinned := out.Bytes() + pinned := bytes.ReplaceAll(manifest, from, []byte(`"`+reference+`"`)) // Read back. A substitution on text can catch more than it was aimed at, and the manifest is // about to be handed to the mesh as the description of what it runs. @@ -172,17 +161,16 @@ func pinImage(manifest []byte, reference, module string) ([]byte, int, error) { return nil, 0, fmt.Errorf( "pinning the %s module's image broke its manifest: %w", module, err) } - if bytes.Contains(pinned, []byte(placeholderDigest)) { + if bytes.Contains(pinned, from) { return nil, 0, fmt.Errorf( - "the %s module's manifest still carries a placeholder digest after pinning", - module) + "the %s module's manifest still names the built image after pinning", module) } return pinned, places, nil } // controlPlaneResourceIn is the id of the resource that runs the control plane. // -// The manifest is written by the catalogue and the installer does not get to name its resources. +// The manifest is the control plane's own and the installer does not get to name its resources. // What it can do is find the one container whose image is the one just pinned — and when a manifest // declares exactly one container, that is the answer without any searching at all. func controlPlaneResourceIn(manifest []byte) string { diff --git a/internal/bootstrap/control_test.go b/internal/bootstrap/control_test.go index ca499f2..7b84df3 100644 --- a/internal/bootstrap/control_test.go +++ b/internal/bootstrap/control_test.go @@ -11,13 +11,15 @@ import ( // that could go wrong quietly: pinning it to the wrong image, and delivering it store connections // the mesh invented rather than the ones the foundation actually made. -// theControlPlaneModule is the catalogue's manifest, trimmed to what this installer reads. +// theControlPlaneModule is the manifest the build produces at step 3: the control plane's own, +// from the root of its repository, its artifact resolved to the image the machine built — named by +// the digest of its own configuration, with no registry in front (novox/hq ADR 0069). // // A fixture rather than the file itself, unlike the foundation example the rewrite tests use: the -// catalogue is a different repository on a different branch, and a test that read it would pass or -// fail according to what somebody else had checked out. What it must stay faithful to is the -// SHAPE — the placeholder digest, the own-secret per context, the mount from the machine's path to -// the container's, and the environment file that fills what is not a path. +// control plane is a different repository on a different branch, and a test that read it would +// pass or fail according to what somebody else had checked out. What it must stay faithful to is +// the SHAPE — the built image's bare id, the own-secret per context, the mount from the machine's +// path to the container's, and the environment file that fills what is not a path. const theControlPlaneModule = `{ "module": "mesh-controller", "version": "1", @@ -37,7 +39,7 @@ const theControlPlaneModule = `{ "mode": "0600", "content": "MESH_BROKER_AMQP=${secret:broker}\nMESH_BROKER_MANAGEMENT=${secret:broker-management}\nMESH_BROKER_ADDRESS=${machine:at}:5671\n"}, {"id": "server", "type": "container", "name": "mesh-controller", - "image": "mesh-controller@` + placeholderDigest + `", + "image": "` + builtImage + `", "network": "host", "args": ["serve"], "env-file": ["/var/lib/mesh/mesh-controller/broker.env"], "env": { @@ -58,57 +60,62 @@ const theControlPlaneModule = `{ const pushedReference = "127.0.0.1:5000/mesh-controller@sha256:" + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" -// **The whole reference moves, not only the digest.** The manifest's placeholder names a -// repository too, and replacing sixty-four zeros inside it would leave `mesh-controller@sha256:…` -// with no registry in front — which a runtime would go to the internet for, and this mesh's -// control plane exists in no public registry by design. +// builtImage is what step 3 built, as the machine that built it names it. +const builtImage = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +// theBuild is what step 3 hands step 9. +func theBuild(manifest string) Built { + return Built{Module: "mesh-controller", Commit: "a1b2c3d4", Image: builtImage, Manifest: []byte(manifest)} +} + +// **The whole reference moves.** The build names its image by the bare digest of its configuration, +// which only the machine that built it can resolve; the mesh's record must name what the registry +// assigned, `/@sha256:…`, or every other machine the module is pushed to +// would go looking for an image nothing serves. func TestTheControlPlaneIsPinnedToWhatThisMeshsRegistryAssigned(t *testing.T) { - pinned, places, err := pinImage([]byte(theControlPlaneModule), pushedReference, "mesh-controller") + pinned, places, err := pinImage([]byte(theControlPlaneModule), builtImage, pushedReference, "mesh-controller") if err != nil { t.Fatal(err) } if places != 1 { - t.Errorf("the placeholder was found in %d place(s)", places) + t.Errorf("the built image was found in %d place(s)", places) } if !strings.Contains(string(pinned), `"image": "`+pushedReference+`"`) { t.Errorf("the manifest does not name the pushed image:\n%s", pinned) } - if strings.Contains(string(pinned), `"mesh-controller@sha256:`) { - t.Errorf("the digest was replaced and the manifest's own repository name was left in "+ - "front of it, so nothing says which registry serves it:\n%s", pinned) + if strings.Contains(string(pinned), builtImage) { + t.Errorf("the built image's bare id survived, which no other machine can resolve:\n%s", pinned) } } -// A manifest already naming a real digest was pinned by somebody else, to some other build. -// Registering it would install a control plane that is not the image this machine just published, -// which is the one thing this step exists to guarantee. -func TestAManifestAlreadyPinnedByHandIsRefused(t *testing.T) { - already := strings.Replace(theControlPlaneModule, placeholderDigest, - "sha256:"+strings.Repeat("9", 64), 1) - if _, _, err := pinImage([]byte(already), pushedReference, "mesh-controller"); err == nil { - t.Fatal("a manifest already pinned to some other image was accepted") +// A manifest naming some other image describes some other build. Registering it would install a +// control plane that is not the image this machine just published, which is the one thing this +// step exists to guarantee. +func TestAManifestNamingAnotherBuildIsRefused(t *testing.T) { + other := strings.Replace(theControlPlaneModule, builtImage, "sha256:"+strings.Repeat("9", 64), 1) + if _, _, err := pinImage([]byte(other), builtImage, pushedReference, "mesh-controller"); err == nil { + t.Fatal("a manifest naming some other image was accepted") } } -// Every placeholder moves. A manifest naming its image in a second resource — a runtime container -// beside the application's, which the catalogue's converted modules routinely carry — would -// otherwise be left half pinned, and fail inside an apply rather than here. +// Every place moves. A manifest naming its image in a second resource — a migrate step beside the +// server — would otherwise be left half pinned, and fail inside an apply rather than here. func TestEveryPlaceTheManifestNamesTheImageIsPinned(t *testing.T) { twice := strings.Replace(theControlPlaneModule, `{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"},`, `{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/mesh-controller", "mode": "0700"}, {"id": "migrate", "type": "container", "name": "mesh-controller-migrate", "run-once": true, - "image": "mesh-controller@`+placeholderDigest+`", "args": ["migrate"]},`, 1) + "image": "`+builtImage+`", "args": ["migrate"]},`, 1) - pinned, places, err := pinImage([]byte(twice), pushedReference, "mesh-controller") + pinned, places, err := pinImage([]byte(twice), builtImage, pushedReference, "mesh-controller") if err != nil { t.Fatal(err) } if places != 2 { - t.Errorf("the placeholder was found in %d place(s), and the manifest names it twice", places) + t.Errorf("the built image was found in %d place(s), and the manifest names it twice", places) } - if strings.Contains(string(pinned), placeholderDigest) { - t.Error("a placeholder survived the pinning") + if strings.Contains(string(pinned), builtImage) { + t.Error("the built image's id survived the pinning") } } @@ -230,7 +237,7 @@ func TestAManifestWantingNoStoresIsRefusedWithTheShapeItShouldHave(t *testing.T) bare := `{"module":"mesh-controller","version":"1","resources":[ {"id":"container","type":"container","name":"mesh-controller", - "image":"mesh-controller@` + placeholderDigest + `"}]}` + "image":"` + builtImage + `"}]}` _, err = deliverStores(context.Background(), Options{Node: "anchor"}, control, []byte(bare), rewritten.Declaration, func(string) {}) @@ -256,9 +263,11 @@ func TestThePermanentControlPlaneIsAskedTheSameQuestion(t *testing.T) { t.Fatal(err) } - out, err := InstallControlPlane(context.Background(), - installing(t, catalogueWith(t, ControlPlaneModule, theControlPlaneModule)), - Deps{Run: runtime.run}, control, rewritten.Declaration, pushedReference, func(string) {}) + // No catalogue: the control plane's manifest is the one the build produced (novox/hq + // 04-ISSUES/072), and step 9 reads nothing from the catalogue any more. + out, err := InstallControlPlane(context.Background(), installing(t, t.TempDir()), + Deps{Run: runtime.run}, control, rewritten.Declaration, theBuild(theControlPlaneModule), + pushedReference, func(string) {}) if err != nil { t.Fatal(err) } diff --git a/internal/bootstrap/module.go b/internal/bootstrap/module.go index 0802e90..4ba2729 100644 --- a/internal/bootstrap/module.go +++ b/internal/bootstrap/module.go @@ -1,7 +1,9 @@ package bootstrap import ( + "bytes" "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -143,3 +145,67 @@ func pushNode(ctx context.Context, o Options, control controlPlane, say func(str say(" pushed " + o.Node) return strings.TrimSpace(said), nil } + +// pinPlaceholder replaces the catalogue's placeholder digest with what the registry assigned — the +// builder's manifest, which the catalogue still holds (the control plane's comes out of its own +// build, see pinImage). +// +// **Textual, and every place it appears.** A manifest may name its image in more than one resource +// — the catalogue's converted modules routinely carry a runtime container beside the application's +// — and the same reasoning as the bundle rewrite applies: replacing one and not the others leaves +// something pointing at an image nothing serves, and it fails half way through an apply rather +// than here. +// +// It refuses a manifest with no placeholder in it. That is not pedantry: a manifest already +// carrying a real digest is one somebody pinned by hand, and quietly registering it would install a +// control plane that is not the image this machine just published — which is the one thing this +// step exists to guarantee. +func pinPlaceholder(manifest []byte, reference, module string) ([]byte, int, error) { + places := bytes.Count(manifest, []byte(placeholderDigest)) + if places == 0 { + return nil, 0, fmt.Errorf( + "the %s module's manifest carries no placeholder digest (%s), so there is nothing to "+ + "pin to the image this machine just published.\n"+ + "A manifest already naming a digest was pinned by somebody else, to some other "+ + "build. Registering it would install a module that is not the one this "+ + "installer carried and pushed", module, placeholderDigest) + } + // The reference the registry gave back is `/@sha256:…`, and what the + // manifest holds is `@sha256:0…0`. Replacing only the digest would leave the + // manifest's own repository name in front of it — which may be `mesh-controller` with no + // registry, and a runtime would then pull it from the internet. The whole reference moves. + var out bytes.Buffer + rest := manifest + for { + at := bytes.Index(rest, []byte(placeholderDigest)) + if at < 0 { + out.Write(rest) + break + } + // Back up over the repository this digest belongs to, which runs to the opening quote. + start := bytes.LastIndexByte(rest[:at], '"') + if start < 0 { + return nil, 0, fmt.Errorf( + "the %s module's manifest has a placeholder digest that is not inside a JSON "+ + "string, so the installer cannot tell what image it belongs to", module) + } + out.Write(rest[:start+1]) + out.WriteString(reference) + rest = rest[at+len(placeholderDigest):] + } + pinned := out.Bytes() + + // Read back. A substitution on text can catch more than it was aimed at, and the manifest is + // about to be handed to the mesh as the description of what it runs. + var checked map[string]any + if err := json.Unmarshal(pinned, &checked); err != nil { + return nil, 0, fmt.Errorf( + "pinning the %s module's image broke its manifest: %w", module, err) + } + if bytes.Contains(pinned, []byte(placeholderDigest)) { + return nil, 0, fmt.Errorf( + "the %s module's manifest still carries a placeholder digest after pinning", + module) + } + return pinned, places, nil +} From 8afbe57814a6992ddc700c640349b90e52125649 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 15:27:13 +0200 Subject: [PATCH 2/3] The pinning line shows the built image's id, not one digit of it --- internal/bootstrap/control.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/control.go b/internal/bootstrap/control.go index 1af4bce..bcade8b 100644 --- a/internal/bootstrap/control.go +++ b/internal/bootstrap/control.go @@ -79,7 +79,7 @@ func InstallControlPlane(ctx context.Context, o Options, d Deps, control control if err != nil { return out, err } - say(fmt.Sprintf(" pinned %s → %s, in %d place(s)", shortRef(built.Image), image, places)) + say(fmt.Sprintf(" pinned %s → %s, in %d place(s)", shortImage(built.Image), image, places)) container, _, err := containerIn(pinned, controlPlaneResourceIn(pinned)) if err != nil { @@ -398,3 +398,13 @@ func sortedKeys(m map[string]string) []string { sort.Strings(keys) return keys } + +// shortImage is an image id as a person reads one: the first twelve hex digits, without the +// algorithm in front — `shortRef` would keep the prefix and show one digit of the digest. +func shortImage(id string) string { + digest := strings.TrimPrefix(id, "sha256:") + if len(digest) > 12 { + return digest[:12] + } + return digest +} From ffe7dbd3484f6c1b892b3a1745a54704138a0e30 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 19:21:04 +0200 Subject: [PATCH 3/3] Step 9 without a build is refused, and a test says so --- internal/bootstrap/control_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/bootstrap/control_test.go b/internal/bootstrap/control_test.go index 7b84df3..ff2fed0 100644 --- a/internal/bootstrap/control_test.go +++ b/internal/bootstrap/control_test.go @@ -307,3 +307,21 @@ func TestABrokerSettingReadFromAFileIsDeliveredToo(t *testing.T) { t.Fatal("a plain path variable was taken for a secret") } } + +// Step 9 with nothing built is a caller's fault, and it stops rather than pretending to pivot. +func TestTheControlPlaneCannotBeInstalledWithoutABuild(t *testing.T) { + runtime := &asked{answer: aMeshThatAgrees(map[string]string{})} + control := controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second} + rewritten, err := Rewrite(theRealBundle(t), held) + if err != nil { + t.Fatal(err) + } + _, err = InstallControlPlane(context.Background(), installing(t, t.TempDir()), + Deps{Run: runtime.run}, control, rewritten.Declaration, Built{}, pushedReference, func(string) {}) + if err == nil || !strings.Contains(err.Error(), "was not built") { + t.Fatalf("a missing build was not refused: %v", err) + } + if runtime.ran("module add") { + t.Error("something was registered without a manifest") + } +}