Files
mesh-host/internal/bootstrap/builder.go
T
jochen d941d798e9
mesh/merge-gate head d941d798e9df merged into trunk d34b4f32e17f — pass: the change touches no module of the mesh's graph
mesh/repo-check head d941d798e9df merged into trunk d34b4f32e17f — pass: its merge-check.sh passed
mesh/delivery delivered
Install the build agent at genesis, the module that builds since ADR 0190 (hq issue 596)
Genesis's builder step read modules/builder from the catalogue, which retired it on 2026-10-03 for
the build agent (node-build-agent on every machine), so no genesis got past step 13. The step now
installs build-agent from the catalogue, puts the builder image it carried and published where the
manifest names the image it builds, and issues its bus account as any module's.
2026-10-13 02:25:10 +02:00

141 lines
5.9 KiB
Go

package bootstrap
import (
"bytes"
"context"
"encoding/json"
"fmt"
)
// BuilderModule is the module that lets a mesh produce anything at all: the build agent, which holds
// the node seat `node-build-agent` (novox/hq ADR 0190). The one-holder `builder` module it replaced
// left the catalogue on 2026-10-03, and genesis went on asking for it (issue 596).
const BuilderModule = "build-agent"
// 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 build agent 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 := pinTheCarriedBuilder(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 build agent that arrives without its credential starts, finds nothing it
// may read, and waits — which looks exactly like an agent with no work. The same act as any
// module's account (`module issue`), which a re-run repeats harmlessly.
if _, err := control.tell(ctx, "module", "issue", BuilderModule, "--node", o.Node); err != nil {
return out, fmt.Errorf("the build agent has no account on the bus, so it can take no work: %w", err)
}
out.Account = true
say(" bus account " + BuilderModule + " on " + o.Node + ", scoped to what it consumes and emits")
out.Installed.Pushed, err = pushNode(ctx, o, control, say)
return out, err
}
// pinTheCarriedBuilder names the image this installer carried and published in the build agent's
// manifest, in place of the artifact the mesh would build for it.
//
// **The one module genesis cannot have built** (novox/hq ADR 0073): the build agent is what builds,
// and at this step the mesh has no build agent. The catalogue's manifest names its server container by
// the artifact it builds (`"artifact": "server"`, an image built from mesh-controller's
// cmd/mesh-builder); the image this installer carries is that build, made from the same program. So
// every container built from that image artifact runs the published reference instead, and the rest
// of the manifest — its build, its seat, its requirements — is the catalogue's as written. The mesh
// rebuilds it from the catalogue like any module from then on.
//
// A manifest still carrying the old placeholder digest is pinned the old way. One naming neither an
// image artifact nor a placeholder is refused: there would be nothing to put the carried image in.
func pinTheCarriedBuilder(manifest []byte, reference string) ([]byte, int, error) {
if bytes.Contains(manifest, []byte(placeholderDigest)) {
return pinPlaceholder(manifest, reference, BuilderModule)
}
var m map[string]any
if err := json.Unmarshal(manifest, &m); err != nil {
return nil, 0, fmt.Errorf("the %s module's manifest is not readable: %w", BuilderModule, err)
}
images := map[string]bool{}
if build, ok := m["build"].(map[string]any); ok {
artifacts, _ := build["artifacts"].([]any)
for _, a := range artifacts {
if art, ok := a.(map[string]any); ok && art["kind"] == "image" {
if name, ok := art["name"].(string); ok {
images[name] = true
}
}
}
}
places := 0
resources, _ := m["resources"].([]any)
for _, r := range resources {
res, ok := r.(map[string]any)
if !ok || res["type"] != "container" {
continue
}
if name, ok := res["artifact"].(string); ok && images[name] {
delete(res, "artifact")
res["image"] = reference
places++
}
}
if places == 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest runs no container from an image it builds, so there is nowhere to "+
"put the builder this installer carried and published (%s)", BuilderModule, reference)
}
pinned, err := json.MarshalIndent(m, "", " ")
if err != nil {
return nil, 0, err
}
return append(pinned, '\n'), places, nil
}