From 8eeb28f00be541219ba6ef514283389df348cd47 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 14 Sep 2026 12:31:43 +0200 Subject: [PATCH] Installation sets up the builder, so a raised mesh can produce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Genesis ended with a mesh that runs and cannot make anything: every module in the catalogue names artifacts and nothing had built them, so the first thing anybody had to do was install a builder by hand. The installer already carries one — it is what built the control plane — so this is the same two acts the control plane goes through, in the same order: publish it, so the mesh names it by a digest its own registry assigned rather than a local identity nothing else can fetch, then install it as an ordinary module pinned to that. And then the part only it needs, a broker account, issued before the push so it arrives with the declaration rather than after it. Verified on a bare machine: the install ends with a builder running, and that mesh then built the shared base images and a module on top of them with nobody helping it. --- cmd/mesh-bootstrap/main.go | 9 ++-- internal/bootstrap/bootstrap.go | 31 +++++++++++- internal/bootstrap/builder.go | 86 +++++++++++++++++++++++++++++++++ internal/bootstrap/publish.go | 22 +++++++-- 4 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 internal/bootstrap/builder.go diff --git a/cmd/mesh-bootstrap/main.go b/cmd/mesh-bootstrap/main.go index 9b9cc33..2d41b74 100644 --- a/cmd/mesh-bootstrap/main.go +++ b/cmd/mesh-bootstrap/main.go @@ -47,7 +47,7 @@ const ( const usage = `mesh-bootstrap — make a bare machine into a mesh - bootstrap the ten steps below (the default) + bootstrap the twelve steps below (the default) version 1 preflight what has to be true before anything is changed @@ -61,6 +61,8 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh 9 publish push the control plane's image into it, for its first digest 10 control reinstall the control plane as an ordinary module, pinned to that digest 11 retire drop the temporary control plane; the host removes it + 12 builder publish the carried builder and install it, so this mesh can + make the rest of the catalogue rather than be handed it --bundle the substrate template to build this machine's bundle from (default ` + defaultTemplate + `) @@ -74,8 +76,9 @@ const usage = `mesh-bootstrap — make a bare machine into a mesh controls, and what is cloned here is the trust anchor for everything this mesh will ever run --source-path the module's directory inside that repository, if not its root - --catalog a checkout of the mesh's catalogue, holding the registry's and the - control plane's manifests. Without it this stops after step 6 + --catalog a checkout of the mesh's catalogue, holding the registry's, the + control plane's and the builder's manifests. Without it this stops + after step 6 --node the name this machine is known by (default: its hostname) --registry where this mesh keeps its own images (default ` + defaultRegistry + `) every node pulls the control plane from this, so on a mesh of more diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 9ded0c8..dc2d0d5 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -52,6 +52,7 @@ const ( StepPublish Step = "publish" StepControlPlane Step = "control-plane" StepRetire Step = "retire" + StepBuilder Step = "builder" ) // Steps in the order they happen, so a failure can say "step 2 of 11". @@ -67,7 +68,7 @@ const ( // mesh made, out of a repository and a commit it can name, and can therefore make again. var Steps = []Step{ StepPreflight, StepLoad, StepBuild, StepBundle, StepApply, StepVerify, - StepEnrol, StepRegistry, StepPublish, StepControlPlane, StepRetire, + StepEnrol, StepRegistry, StepPublish, StepControlPlane, StepRetire, StepBuilder, } // Error is a failure, named by the step it happened in. @@ -182,6 +183,12 @@ type Result struct { ImageTags []string `json:"image-tags,omitempty"` // ImageHeld is true when the machine already held it and nothing was loaded. ImageHeld bool `json:"image-already-held,omitempty"` + // BuilderPublished is where the builder's image ended up, and BuilderInstalled whether it is + // a module on this machine. A mesh without them runs and cannot produce. + BuilderPublished string `json:"builder-image,omitempty"` + BuilderInstalled bool `json:"builder-installed,omitempty"` + BuilderAccount bool `json:"builder-account-issued,omitempty"` + // Built is what the genesis build produced, and BuiltFrom is the commit it actually built. // // Reported because they are the difference between a mesh that can rebuild its control plane @@ -500,7 +507,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro return result, failed(StepControlPlane, err) } - // ---- 10. retire ----------------------------------------------------------------------- + // ---- 11. retire ----------------------------------------------------------------------- say("retire — the temporary control plane is dropped from the bundle") retired, err := RetireTheTemporaryControlPlane(ctx, o, sys, rewritten.Bundle, d.Run, say) result.TemporaryRetired = retired.Gone || retired.Already @@ -509,8 +516,28 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro return result, failed(StepRetire, err) } + // ---- 12. builder ---------------------------------------------------------------------- + // + // **Last, and part of installing rather than after it.** What the steps above produce is a mesh + // that runs and cannot make anything — every module in the catalogue names artifacts and + // nothing has built them. The builder is carried in this installer, because it is what built + // the control plane; putting it in the registry and installing it as a module is what turns a + // mesh that runs into a mesh that can produce. + say("builder — the mesh gets the thing that makes everything else") + // The PERMANENT control plane, not the temporary one: by here the temporary is gone, and the + // module this installs is assigned through the thing that will still be running afterwards. + permanentControl := controlPlane{container: ControlPlaneModule, run: d.Run, timeout: o.Timeout} + builder, err := InstallBuilder(ctx, o, d, permanentControl, loaded.ID, say) + result.BuilderPublished = builder.Published.Reference + result.BuilderInstalled = builder.Installed.Assigned || builder.Installed.Known + result.BuilderAccount = builder.Account + if err != nil { + return result, failed(StepBuilder, err) + } + say("\nthis machine is a mesh of one node, and the control plane it runs is a module " + "pinned to an image its own registry serves.") + say("it holds a builder, so it can make the rest of the catalogue rather than be handed it.") say("what remains is somebody else's: adding nodes, and assigning what they should run.") return result, nil diff --git a/internal/bootstrap/builder.go b/internal/bootstrap/builder.go new file mode 100644 index 0000000..865bc83 --- /dev/null +++ b/internal/bootstrap/builder.go @@ -0,0 +1,86 @@ +package bootstrap + +import ( + "context" + "fmt" + "strings" +) + +// BuilderModule is the module that lets a mesh produce anything at all. +const BuilderModule = "builder" + +// BuilderRepository is what its image is called in this mesh's own registry. +const BuilderRepository = "mesh-builder" + +// Builder is what installing it produced. +type Builder struct { + Published Published + Installed Installed + // Account is true when a broker account was issued for it here. + Account bool +} + +// InstallBuilder gives a fresh mesh the thing that makes everything else. +// +// **Without this a mesh can run and cannot produce** (novox/hq ADR 0073). Genesis ends with a +// control plane, a store, a queue and a registry — and almost every module in the catalogue is +// waiting to be built, because a manifest names artifacts and nothing has made them. The builder is +// one of the few things that cannot be built by the thing it is, so it is carried; and it is +// already here, because it is what built the control plane. +// +// So this is the same two acts the control plane went through, in the same order and for the same +// reason: publish the image so the mesh names it by a digest its own registry assigned rather than +// by a local identity nothing else can fetch, then install it as an ordinary module pinned to that. +// +// And then the part only it needs: a broker account. A builder takes work from a queue and +// announces what it made, and it holds its own credential for that like any module — asking for a +// generic one produced an account that could do neither, which is what made this worth its own step +// rather than a line in another. +func InstallBuilder(ctx context.Context, o Options, d Deps, control controlPlane, imageID string, + say func(string)) (Builder, error) { + + var out Builder + + published, err := publishAs(ctx, o, d, imageID, BuilderRepository, say) + out.Published = published + if err != nil { + return out, err + } + + manifest, err := readManifest(o.Catalogue, BuilderModule) + if err != nil { + return out, fmt.Errorf("%w\n"+ + "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) + if err != nil { + return out, err + } + say(fmt.Sprintf(" pinned to %s, named in %d place(s)", published.Reference, places)) + + installed, err := registerAndAssign(ctx, o, control, BuilderModule, pinned, say) + out.Installed = installed + if err != nil { + return out, err + } + + // Before the push, so the account is in the declaration the machine receives rather than in + // the one after it. A builder that arrives without its credential starts, finds nothing it may + // read, and waits — which looks exactly like a builder with no work. + account := o.Node + "-" + BuilderModule + if _, err := control.tell(ctx, "builder", "issue", account, "--node", o.Node); err != nil { + // Said and carried on. An account that already exists is the ordinary case on a re-run, + // and the push below is what makes either state true on the machine. + if !strings.Contains(err.Error(), "already") { + return out, fmt.Errorf("the builder has no broker account, so it can take no work: %w", err) + } + say(" broker account " + account + " — already issued") + } else { + out.Account = true + say(" broker account " + account + ", scoped to what it consumes and emits") + } + + out.Installed.Pushed, err = pushNode(ctx, o, control, say) + return out, err +} diff --git a/internal/bootstrap/publish.go b/internal/bootstrap/publish.go index e7288b1..5ad5af9 100644 --- a/internal/bootstrap/publish.go +++ b/internal/bootstrap/publish.go @@ -57,7 +57,19 @@ type Published struct { func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string, say func(string)) (Published, error) { - remote := o.Registry + "/" + ControlPlaneRepository + return publishAs(ctx, o, d, imageID, ControlPlaneRepository, say) +} + +// publishAs puts one locally held image into this mesh's registry, under a repository name. +// +// **The same act for every image genesis has to place**, which is now two: the control plane it +// built, and the builder it carried. They arrive differently and are published identically — the +// registry does not care where an image came from, and a second copy of this that drifted would be +// the kind of difference nobody finds until one of them stops working. +func publishAs(ctx context.Context, o Options, d Deps, imageID, repository string, + say func(string)) (Published, error) { + + remote := o.Registry + "/" + repository out := Published{Tagged: remote + ":" + genesisTag} // Asked first. A digest already served is a fact about the registry, and re-pushing an image @@ -72,7 +84,7 @@ func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string, } if _, err := d.Run(ctx, "docker", "tag", imageID, out.Tagged); err != nil { - return out, fmt.Errorf("cannot tag the carried image as %s: %w", out.Tagged, err) + return out, fmt.Errorf("cannot tag %s as %s: %w", imageID, out.Tagged, err) } if _, err := d.Run(ctx, "docker", "push", out.Tagged); err != nil { return out, fmt.Errorf( @@ -93,9 +105,9 @@ func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string, if pinned == "" { return out, fmt.Errorf( "%s was pushed and the registry does not serve it.\n"+ - "The next step names the control plane's module by the digest this was supposed to "+ - "produce, so there is nothing to name. Check `docker push` and "+ - "http://%s/v2/%s/tags/list", out.Tagged, o.Registry, ControlPlaneRepository) + "The next step names this module by the digest this was supposed to produce, so "+ + "there is nothing to name. Check `docker push` and "+ + "http://%s/v2/%s/tags/list", out.Tagged, o.Registry, repository) } out.Reference = pinned say(" published " + pinned) -- 2.54.0