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.
141 lines
5.9 KiB
Go
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
|
|
}
|