Files
mesh-host/internal/bootstrap/build.go
T
jschoubben e1a2fe7323 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.
2026-09-13 04:08:58 +02:00

166 lines
6.0 KiB
Go

package bootstrap
import (
"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
}
// builderOutput is the part of the builder's one-shot result this needs.
type builderOutput struct {
Module string `json:"module"`
Commit string `json:"commit"`
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))
}
say(fmt.Sprintf(" built %s from %s", result.Module, shortRef(result.Commit)))
return Built{Module: result.Module, Commit: result.Commit, Image: images[0]}, 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
}