The installer carries a builder and builds the control plane it raises

It carried the thing it was going to run; it now carries the thing that makes
it. One artifact either way — but a mesh raised this way holds a control plane
it built from a repository and a commit it can name, and can therefore build
again. A mesh handed a finished image could not, and had no way to find that
out until somebody needed it to.

A build step sits between load and bundle, because the bundle must name an
image and that image no longer arrives finished. Everything after it is
unchanged: a locally built image is named by the digest of its own
configuration, which is exactly what the carried one was named by.

Refused in preflight when nothing says what to build, so a run that cannot
finish says so before it has changed anything.
This commit is contained in:
2026-09-13 04:08:58 +02:00
parent cab83b61c8
commit e1a2fe7323
10 changed files with 312 additions and 37 deletions
+42 -9
View File
@@ -43,6 +43,7 @@ type Step string
const (
StepPreflight Step = "preflight"
StepLoad Step = "load"
StepBuild Step = "build"
StepBundle Step = "bundle"
StepApply Step = "apply"
StepVerify Step = "verify"
@@ -53,14 +54,19 @@ const (
StepRetire Step = "retire"
)
// Steps in the order they happen, so a failure can say "step 2 of 10".
// Steps in the order they happen, so a failure can say "step 2 of 11".
//
// The first five make a machine; the last five make a mesh that can maintain itself. They are one
// program because they are one procedure — the whole reason the pivot exists is that steps 7 to 9
// cannot happen without steps 1 to 5, and steps 1 to 5 leave something that cannot be upgraded
// without steps 7 to 9 (novox/hq ADR 0067).
// The first six make a machine; the last five make a mesh that can maintain itself. They are one
// program because they are one procedure — the whole reason the pivot exists is that steps 8 to 10
// cannot happen without steps 1 to 6, and steps 1 to 6 leave something that cannot be upgraded
// without steps 8 to 10 (novox/hq ADR 0067).
//
// **Build sits between load and bundle**, because the bundle has to name an image and that image
// no longer arrives finished. The installer carries the builder, loads it, and uses it to produce
// the control plane from source (novox/hq ADR 0073) — so what the bundle names is something this
// mesh made, out of a repository and a commit it can name, and can therefore make again.
var Steps = []Step{
StepPreflight, StepLoad, StepBundle, StepApply, StepVerify,
StepPreflight, StepLoad, StepBuild, StepBundle, StepApply, StepVerify,
StepEnrol, StepRegistry, StepPublish, StepControlPlane, StepRetire,
}
@@ -118,6 +124,11 @@ type Options struct {
// looks installed and cannot upgrade itself.
Catalogue string
// Source is where the control plane is built from — a repository on a mesh that already
// exists, and a commit. The installer carries the builder rather than a finished control
// plane (novox/hq ADR 0073), so this is what it is told to make.
Source Source
// Registry is where this mesh's own images live, as this machine reaches it. Every node will
// pull the control plane from what this says, so on a mesh of more than one machine it must be
// an address the others can reach.
@@ -171,6 +182,14 @@ 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"`
// 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
// and one that cannot: a machine holding these can be asked for the same thing again and get
// the same thing back.
Built string `json:"built,omitempty"`
BuiltFrom string `json:"built-from,omitempty"`
// ImagePredicted is true when Image is the archive's id because nothing was loaded — a dry run
// only, and the reason a dry run does not claim to know what would be applied.
ImagePredicted bool `json:"image-id-is-a-prediction,omitempty"`
@@ -287,7 +306,7 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
say(" system " + sys.Name())
// ---- 2. load ------------------------------------------------------------------------
say("load — the control plane's image, carried in this installer")
say("load — the builder's image, carried in this installer")
loaded, err := Load(ctx, d.Run, o.DryRun, say)
if err != nil {
return result, failed(StepLoad, err)
@@ -296,9 +315,23 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
result.ImageArchive, result.ImageTag = loaded.Archive, loaded.Tag
result.ImagePredicted = loaded.Predicted
// ---- 3. bundle ----------------------------------------------------------------------
// ---- 3. build -----------------------------------------------------------------------
say("build — the control plane, from its own repository and a commit")
built, err := BuildControlPlane(ctx, d.Run, loaded.Tag, o.Source, o.DryRun, say)
if err != nil {
return result, failed(StepBuild, err)
}
result.Built, result.BuiltFrom = built.Module, built.Commit
controlPlaneImage := built.Image
if o.DryRun {
// Nothing was built, so there is no id to name. The carried builder's own is used only so
// the remaining steps have something well-formed to describe; nothing is applied.
controlPlaneImage = loaded.ID
}
// ---- 4. bundle ----------------------------------------------------------------------
say("bundle — what this machine will be asked to be")
rewritten, err := Rewrite(template, loaded.ID)
rewritten, err := Rewrite(template, controlPlaneImage)
if err != nil {
return result, failed(StepBundle, err)
}