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 }