Files
mesh-host/internal/bootstrap/bootstrap.go
T
jschoubben 9c9e02ba1d mesh-bootstrap: drop an unread parameter
A parameter nothing reads is a claim the function makes about what it needs, and
this one was wrong.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 23:18:07 +02:00

261 lines
11 KiB
Go

// Package bootstrap brings a mesh into existence on a bare machine.
//
// **Why this is a program at all.** Until now the only complete written-down copy of the
// first-node procedure was an integration test in the lab — `whole-mesh-full.test.ts` — which is
// why every gap in it kept being found late and by accident: an install procedure that lives as a
// test fixture is exercised by whoever is writing tests, never by whoever is installing. This is
// that procedure, made into the thing it always was.
//
// **Tier 0, and a separate binary.** Bootstrapping is done by hand and it changes a machine, so by
// novox/hq 03-DESIGN/01-to-be/05-the-node-host.md it is tier 0 and belongs beside the host. It is
// not a `mesh-host` subcommand, because `mesh-host` says of itself that it connects to nothing and
// listens on nothing and that what it applies comes from a file — a property that is what makes an
// always-running root daemon auditable, and that must stay literally true. This program pulls
// images and asks a running control plane questions. Same tier, same repository, different binary.
//
// **No registry is required for the mesh's own image, and no source either.** The control plane
// exists in no registry by design, and the forge that holds its source runs on the mesh — so a
// bootstrap that fetched or built it would need a mesh in order to raise a mesh. The installer
// carries the image (`internal/image`) and names it by its image id: the sha256 of its own
// configuration, which is exact, unforgeable, and needs nothing to have served it (novox/hq
// ADR 0006, and `internal/declaration`'s checkImage). Third-party images keep their upstream
// `name@sha256:` references and are pulled from the internet like anything else.
package bootstrap
import (
"context"
"fmt"
"time"
)
// Step names one stage. A failure says which one, because "the bootstrap failed" is a sentence
// nobody can act on and this will be run over and over by somebody getting a machine working.
type Step string
const (
StepPreflight Step = "preflight"
StepLoad Step = "load"
StepBundle Step = "bundle"
StepApply Step = "apply"
StepVerify Step = "verify"
)
// Steps in the order they happen, so a failure can say "step 2 of 5".
var Steps = []Step{StepPreflight, StepLoad, StepBundle, StepApply, StepVerify}
// Error is a failure, named by the step it happened in.
type Error struct {
Step Step
Err error
}
func (e *Error) Error() string {
at := 0
for i, s := range Steps {
if s == e.Step {
at = i + 1
}
}
return fmt.Sprintf("step %d of %d, %s: %v", at, len(Steps), e.Step, e.Err)
}
func (e *Error) Unwrap() error { return e.Err }
func failed(step Step, err error) error {
if err == nil {
return nil
}
return &Error{Step: step, Err: err}
}
// Options are the things that differ between machines.
type Options struct {
// Template is the substrate bundle this machine's own bundle is made from.
Template string
// Out is where the produced bundle is written, so a person can read what was applied.
Out string
// State is where the host records what it has applied here — the same file `mesh-host` reads,
// because what this raises the host must afterwards own.
State string
// System is which half of the host applies things. Empty means ask the machine.
System string
// DryRun does everything that does not change the machine.
DryRun bool
// Timeout bounds any single probe.
Timeout time.Duration
// Wait is how long something that is merely starting is given: a socket-activated container
// runtime, a control plane opening its stores.
Wait time.Duration
}
// Deps are the ways this program reaches outside itself. Injected so the whole of it can be
// tested without a container runtime, a network, or a machine to break — the same reason
// `internal/apply` takes a Runner (novox/hq ADR 0017).
type Deps struct {
// Run executes a command. apply.ExecRunner in production.
Run Runner
// Dial reports whether a TCP address answers, for "can this machine reach the registries the
// bundle names".
Dial func(ctx context.Context, address string) error
}
// Result is what the bootstrap did, in the shape `--json` prints.
type Result struct {
System string `json:"system"`
DryRun bool `json:"dry-run,omitempty"`
// Image is the control plane's image id — what the produced bundle names it by.
Image string `json:"image,omitempty"`
// ImageTags is what that image was called when it was saved. Decoration, for a person.
ImageTags []string `json:"image-tags,omitempty"`
// ImageHeld is true when the machine already held it and nothing was loaded.
ImageHeld bool `json:"image-already-held,omitempty"`
// Bundle is where the produced bundle was written, and what was done to produce it.
Bundle string `json:"bundle,omitempty"`
BundleWas string `json:"bundle-replaced,omitempty"`
BundlePlaces int `json:"bundle-places,omitempty"`
BundleWrote bool `json:"bundle-written,omitempty"`
// Applied is how many resources the apply reported on, and whether any of them moved.
Applied int `json:"applied,omitempty"`
Changed bool `json:"changed,omitempty"`
// Running is the substrate's containers, confirmed up.
Running []string `json:"running,omitempty"`
// Answered is what the control plane said back — not merely that it is up.
Answered string `json:"control-plane,omitempty"`
// Stopped names why a dry run went no further. Empty on a real run.
Stopped string `json:"stopped,omitempty"`
}
// Run performs the bootstrap, saying what it is doing as it goes.
//
// Every step is idempotent, and every step says whether it found something or changed it. That is
// not politeness: this program is run repeatedly while somebody gets a machine working, and a step
// that cannot tell "already done" from "just done" makes the second run indistinguishable from the
// first — which is how a person stops believing any of it.
//
// It does not retry. A pull that failed for a reason that goes away by itself is real, and the
// answer to it is to run this again: re-running is the retry, and it is one a person chooses after
// reading which step failed and why.
//
// **What this does NOT do: enrolment, the module catalogue, and assignment.** It stops at a running
// substrate with a control plane that replies — a mesh of one node with nothing joined to it. See
// the marker at the end.
func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, error) {
if say == nil {
say = func(string) {}
}
result := Result{DryRun: o.DryRun}
// ---- 1. preflight -------------------------------------------------------------------
say("preflight — what has to be true before anything is changed")
template, err := Preflight(ctx, o, d, say)
if err != nil {
return result, failed(StepPreflight, err)
}
// Which half of the host applies things here. Asked of the machine and proved, because
// `mesh-host` pins this at link time and an installer run by hand has no link time.
sys, err := WorkOutSystem(ctx, d.Run, o.System)
if err != nil {
return result, failed(StepPreflight, err)
}
result.System = sys.Name()
say(" system " + sys.Name())
// ---- 2. load ------------------------------------------------------------------------
say("load — the control plane's image, carried in this installer")
loaded, err := Load(ctx, d.Run, o.DryRun, say)
if err != nil {
return result, failed(StepLoad, err)
}
result.Image, result.ImageTags, result.ImageHeld = loaded.ID, loaded.Tags, loaded.Held
// ---- 3. bundle ----------------------------------------------------------------------
say("bundle — what this machine will be asked to be")
rewritten, err := Rewrite(template, loaded.ID)
if err != nil {
return result, failed(StepBundle, err)
}
result.BundleWas, result.BundlePlaces, result.Bundle = rewritten.Was, rewritten.Places, o.Out
if rewritten.Changed {
say(fmt.Sprintf(" control plane %s", rewritten.Now))
say(fmt.Sprintf(" replacing %s, named in %d place(s)",
rewritten.Was, rewritten.Places))
} else {
say(fmt.Sprintf(" control plane %s — the template already named it, nothing rewritten",
rewritten.Now))
}
for _, kept := range rewritten.Kept {
say(" left alone " + kept)
}
if rewritten.BrokerAddress != "" {
// Said every time, and never changed. Every enrolment token this mesh issues will tell a
// joining node to dial this address, and a wrong one is silent until the second node fails
// to come back. The installer does not know this machine's address and will not invent it.
say(" nodes will dial " + rewritten.BrokerAddress +
" — check this is an address other machines can reach")
}
if o.DryRun {
// Nothing is written, exactly as `mesh-host --dry-run` reads a declaration and refuses it
// if wrong while changing nothing. The bundle has been produced and re-parsed in memory,
// which is everything that could be checked without touching the machine; what is left is
// loading, applying and asking the result questions, and none of those can be answered by
// not doing them.
say(fmt.Sprintf(" would write %s (%d resources)", o.Out, rewritten.Resources))
result.Stopped = "dry run: the bundle was produced and checked, and nothing was written, " +
"loaded or applied"
say("\n" + result.Stopped)
return result, nil
}
if err := writeBundleFile(o.Out, rewritten.Bundle); err != nil {
return result, failed(StepBundle, err)
}
result.BundleWrote = true
say(fmt.Sprintf(" wrote %s (%d resources) — read it, this is what is applied",
o.Out, rewritten.Resources))
// ---- 4. apply -----------------------------------------------------------------------
say("apply — raising the substrate")
report, err := ApplyBundle(ctx, o, sys, rewritten.Declaration, d.Run, say)
result.Applied, result.Changed = len(report.Outcomes), report.Changed()
if err != nil {
return result, failed(StepApply, err)
}
if report.Changed() {
say(fmt.Sprintf(" applied %d resource(s)", len(report.Outcomes)))
} else {
say(fmt.Sprintf(" already matches %d resource(s) checked, nothing moved",
len(report.Outcomes)))
}
// ---- 5. verify ----------------------------------------------------------------------
say("verify — the substrate is up, and the control plane replies")
verified, err := Verify(ctx, rewritten.Declaration, d.Run, o.Timeout, o.Wait, say)
result.Running, result.Answered = verified.Running, verified.Answered
if err != nil {
return result, failed(StepVerify, err)
}
say("\nthis machine is a mesh of one node, with nothing joined to it yet.")
// NEXT STAGE — NOT IMPLEMENTED HERE.
//
// What remains between "a mesh exists" and "a mesh does something": issuing this machine a
// token and enrolling it as its own first node, registering the module catalogue with the
// control plane, and assigning modules to nodes. All three are conversations with the control
// plane that has just been proved to reply, so they belong after this point and inside none of
// the steps above.
//
// Left out rather than half-written. Everything above changes a machine; all of that changes a
// mesh, and a program that did both would have two jobs and one name.
say("not done here: enrolment, the module catalogue, and assignment.")
return result, nil
}