Files
mesh-host/internal/bootstrap/build.go
jschoubben 5223169226 Genesis registers the control plane with the manifest its build produced
The control plane's manifest existed twice: at the root of its repository, read
whenever the mesh rebuilds it from source, and as a copy in the catalogue, read by
genesis. Nothing kept them equal, and the first rebuild replaced the mesh's record
with the repository's shape while every later push was refused (novox/hq
04-ISSUES/072). The builder's one-shot result already carries the manifest it built,
artifact resolved to the image; step 3 keeps it and step 9 registers it, re-pinning
the built image's bare id to the reference the registry assigned. The catalogue is
still read for the registry's and the builder's manifests and for phase two.
2026-09-21 15:17:47 +02:00

190 lines
7.4 KiB
Go

package bootstrap
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Source is where the control plane is built from.
//
// **A commit, not a branch** (novox/hq ADR 0071). The forge a mesh installs from is the trust
// anchor for everything that mesh will ever run, and a branch is a moving target somebody else
// controls. The installer names what it wants and the builder checks what it got.
type Source struct {
// Repository is the clone URL, on a mesh that already exists. Not the one being raised.
Repository string
// Ref is the commit to build.
Ref string
// Path is the module's directory inside that repository. Empty is its root.
Path string
}
// Named reports whether a source was given at all.
func (s Source) Named() bool { return strings.TrimSpace(s.Repository) != "" }
// Check is whether this installer was told enough to build anything.
//
// **Its own function so it can be asked twice**: once in preflight, before the machine has been
// touched, and once at the build, which is where it would otherwise be discovered. The first is
// what a person wants — a run that cannot finish should say so before it changes anything — and
// the second is what keeps the build honest if it is ever called from somewhere else.
func (s Source) Check() error {
if !s.Named() {
return errors.New(
"this installer carries a builder and was not told what to build. Give it --source " +
"(a repository on a mesh that already exists) and --source-ref (a commit). It " +
"does not guess: what it clones is the trust anchor for everything this mesh " +
"will ever run")
}
if strings.TrimSpace(s.Ref) == "" {
return errors.New(
"--source was given without --source-ref. Genesis names a commit, because a branch " +
"is a moving target somebody else controls and what is cloned here is the trust " +
"anchor for everything this mesh will ever run (novox/hq ADR 0071)")
}
return nil
}
// Built is what one genesis build produced.
type Built struct {
// Module is what the manifest called itself, so the installer can say it built the right thing.
Module string
// Commit is what was actually built, which may not be what was asked for if a ref moved.
Commit string
// Image is the artifact, named by the digest of its own configuration — the identity a machine
// can use with nothing serving it, and the same one the installer used for a carried image.
Image string
// Manifest is the module as the mesh should hold it: the manifest at the root of the repository
// that was built, its artifact resolved to Image. It is the control plane's ONE manifest
// (novox/hq ADR 0069) — the installer used to read a second copy out of the catalogue, and the
// two drifted apart the first time somebody edited one (novox/hq 04-ISSUES/072).
Manifest []byte
}
// builderOutput is the part of the builder's one-shot result this needs.
type builderOutput struct {
Module string `json:"module"`
Commit string `json:"commit"`
Manifest json.RawMessage `json:"manifest"`
Made []struct {
Name string `json:"name"`
Kind string `json:"kind"`
Reference string `json:"reference"`
} `json:"made"`
}
// BuildControlPlane runs the carried builder once, to produce the control plane from source.
//
// **This is the step that makes a raised mesh able to maintain itself** (novox/hq ADR 0073). What
// comes out is not merely an image: it came from a named repository, a path and a commit, which is
// the same description every later rebuild of the control plane will use. A mesh raised this way
// can rebuild the thing that runs it. A mesh handed a finished image cannot, and has no way to
// discover that until somebody needs it to.
//
// The builder is given the machine's container runtime and nothing else. It is not given a registry:
// there is none yet, and none is needed — the image it produces stays in the runtime of the machine
// that will run it, which is this one.
func BuildControlPlane(ctx context.Context, run Runner, builderTag string, source Source,
dryRun bool, say func(string)) (Built, error) {
if err := source.Check(); err != nil {
return Built{}, err
}
args := []string{
"run", "--rm",
// The build runs containers of its own, which is the whole of what it needs.
"-v", "/var/run/docker.sock:/var/run/docker.sock",
builderTag,
"build", source.Repository, "--ref", source.Ref,
}
if source.Path != "" {
args = append(args, "--path", source.Path)
}
say(fmt.Sprintf("building the control plane from %s at %s", source.Repository, shortRef(source.Ref)))
if dryRun {
say(" dry run: not built")
return Built{}, nil
}
out, err := run(ctx, "docker", args...)
if err != nil {
return Built{}, fmt.Errorf("the control plane could not be built from %s at %s: %w",
source.Repository, shortRef(source.Ref), err)
}
// The builder writes its result to standard output and everything else to standard error, so
// what is parsed here is the whole of what it said. Trimmed rather than searched: a parser that
// hunts for the first `{` will happily read a brace out of a progress line.
var result builderOutput
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &result); err != nil {
return Built{}, fmt.Errorf(
"the builder finished and what it said is not a result: %w. What it said was: %s",
err, firstLine(out))
}
var images []string
for _, made := range result.Made {
if made.Kind == "image" {
images = append(images, made.Reference)
}
}
switch len(images) {
case 1:
case 0:
return Built{}, fmt.Errorf(
"%s built, and produced no image. The installer raises the control plane from an "+
"image, so there is nothing here to raise", result.Module)
default:
// Refused rather than guessed at. Picking one of several would work until the day the
// order changed, and then raise the wrong thing without saying so.
return Built{}, fmt.Errorf(
"%s produced %d images, and the installer cannot tell which one is the control "+
"plane. A module raised at genesis declares exactly one",
result.Module, len(images))
}
// The manifest is what the mesh will hold the control plane as, so a result without one is
// a build the installer cannot finish — refused here, beside the builder that said it, rather
// than at step 9 with a message about a missing file.
manifest := bytes.TrimSpace(result.Manifest)
if len(manifest) == 0 || bytes.Equal(manifest, []byte("null")) {
return Built{}, fmt.Errorf(
"%s built, and the builder reported no manifest for it. The installer registers the "+
"control plane with the manifest the build produced — the one at the root of its "+
"repository, its artifact resolved — and has no other copy to use", result.Module)
}
if !bytes.Contains(manifest, []byte(`"`+images[0]+`"`)) {
return Built{}, fmt.Errorf(
"%s built %s, and the manifest the builder reported does not name that image, so "+
"the installer cannot tell which of its resources runs the control plane",
result.Module, images[0])
}
say(fmt.Sprintf(" built %s from %s", result.Module, shortRef(result.Commit)))
return Built{Module: result.Module, Commit: result.Commit, Image: images[0], Manifest: manifest}, nil
}
func shortRef(ref string) string {
if len(ref) > 8 {
return ref[:8]
}
return ref
}
func firstLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
if len(s) > 200 {
return s[:200]
}
return s
}