Merge pull request 'Installation sets up the builder, so a raised mesh can produce' (#10) from feat/installation-sets-up-the-builder into main

This commit was merged in pull request #10.
This commit is contained in:
2026-09-14 12:32:04 +02:00
4 changed files with 138 additions and 10 deletions
+6 -3
View File
@@ -47,7 +47,7 @@ const (
const usage = `mesh-bootstrap — make a bare machine into a mesh 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 version
1 preflight what has to be true before anything is changed 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 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 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 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 --bundle the substrate template to build this machine's bundle from
(default ` + defaultTemplate + `) (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 controls, and what is cloned here is the trust anchor for
everything this mesh will ever run everything this mesh will ever run
--source-path the module's directory inside that repository, if not its root --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 --catalog a checkout of the mesh's catalogue, holding the registry's, the
control plane's manifests. Without it this stops after step 6 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) --node the name this machine is known by (default: its hostname)
--registry where this mesh keeps its own images (default ` + defaultRegistry + `) --registry where this mesh keeps its own images (default ` + defaultRegistry + `)
every node pulls the control plane from this, so on a mesh of more every node pulls the control plane from this, so on a mesh of more
+29 -2
View File
@@ -52,6 +52,7 @@ const (
StepPublish Step = "publish" StepPublish Step = "publish"
StepControlPlane Step = "control-plane" StepControlPlane Step = "control-plane"
StepRetire Step = "retire" StepRetire Step = "retire"
StepBuilder Step = "builder"
) )
// Steps in the order they happen, so a failure can say "step 2 of 11". // 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. // mesh made, out of a repository and a commit it can name, and can therefore make again.
var Steps = []Step{ var Steps = []Step{
StepPreflight, StepLoad, StepBuild, StepBundle, StepApply, StepVerify, 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. // Error is a failure, named by the step it happened in.
@@ -182,6 +183,12 @@ type Result struct {
ImageTags []string `json:"image-tags,omitempty"` ImageTags []string `json:"image-tags,omitempty"`
// ImageHeld is true when the machine already held it and nothing was loaded. // ImageHeld is true when the machine already held it and nothing was loaded.
ImageHeld bool `json:"image-already-held,omitempty"` 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. // 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 // 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) return result, failed(StepControlPlane, err)
} }
// ---- 10. retire ----------------------------------------------------------------------- // ---- 11. retire -----------------------------------------------------------------------
say("retire — the temporary control plane is dropped from the bundle") say("retire — the temporary control plane is dropped from the bundle")
retired, err := RetireTheTemporaryControlPlane(ctx, o, sys, rewritten.Bundle, d.Run, say) retired, err := RetireTheTemporaryControlPlane(ctx, o, sys, rewritten.Bundle, d.Run, say)
result.TemporaryRetired = retired.Gone || retired.Already 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) 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 " + 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.") "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.") say("what remains is somebody else's: adding nodes, and assigning what they should run.")
return result, nil return result, nil
+86
View File
@@ -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
}
+17 -5
View File
@@ -57,7 +57,19 @@ type Published struct {
func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string, func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string,
say func(string)) (Published, error) { 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} out := Published{Tagged: remote + ":" + genesisTag}
// Asked first. A digest already served is a fact about the registry, and re-pushing an image // 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 { 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 { if _, err := d.Run(ctx, "docker", "push", out.Tagged); err != nil {
return out, fmt.Errorf( return out, fmt.Errorf(
@@ -93,9 +105,9 @@ func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string,
if pinned == "" { if pinned == "" {
return out, fmt.Errorf( return out, fmt.Errorf(
"%s was pushed and the registry does not serve it.\n"+ "%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 "+ "The next step names this module by the digest this was supposed to produce, so "+
"produce, so there is nothing to name. Check `docker push` and "+ "there is nothing to name. Check `docker push` and "+
"http://%s/v2/%s/tags/list", out.Tagged, o.Registry, ControlPlaneRepository) "http://%s/v2/%s/tags/list", out.Tagged, o.Registry, repository)
} }
out.Reference = pinned out.Reference = pinned
say(" published " + pinned) say(" published " + pinned)