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.
190 lines
7.4 KiB
Go
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
|
|
}
|