Files
mesh-host/internal/bootstrap/apply.go
T
jschoubben b82ab95f74 mesh-bootstrap: the first-node procedure, as a program rather than a test
The only complete written-down copy of how a mesh is stood up was an integration
test in the lab. That is why every bootstrap gap kept being found late: an install
procedure that lives as a test fixture is exercised by whoever writes tests, never
by whoever installs. This is that procedure.

A separate binary, not a mesh-host subcommand. mesh-host says of itself that it
connects to nothing and listens on nothing and that what it applies comes from a
file, and that sentence is what makes an always-running root daemon auditable. An
installer loads images and interrogates a control plane. Same tier, different
program.

The control plane's image is carried, not built and not fetched. The forge that
holds its source runs on the mesh, so a bootstrap that had to fetch it would need
a mesh in order to raise one. Embedding breaks that cycle the way the carried
bundle breaks "copy it onto a machine and run it". The image id is read out of the
saved tar before the runtime is asked anything, which is what makes the load
idempotent: the installer can ask whether the machine already holds exactly this.

Five steps, each idempotent and each saying whether it found or changed something,
because this is run over and over by somebody getting a machine working. It stops
at a running substrate with a control plane that replies — enrolment, the module
catalogue and assignment are the next stage and are deliberately absent.

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

135 lines
5.7 KiB
Go

package bootstrap
import (
"context"
"errors"
"fmt"
"strings"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/store"
"github.com/novox/mesh-host/internal/system"
)
// Runner is the same runner every applier in this repository takes.
type Runner = apply.Runner
// ApplyBundle raises the substrate, through the host's own apply.
//
// **This calls `internal/apply` rather than running the `mesh-host` binary**, and that is worth
// stating because shelling out would have been easier. The installer and the host must apply a
// declaration identically — same removal pass, same read-backs, same refusal model, same record of
// what this machine now owns — and two code paths that must behave the same are two code paths
// that will not. The `mesh-host` binary is also not guaranteed to be on a machine this program is
// raising, which would make the installer depend on the thing it installs.
//
// It applies under `store.OriginCarried`, which is the same origin `mesh-host reconcile` uses and
// is not a detail: what the substrate raised must be invisible to the removal pass of a
// declaration that later arrives from the control plane, or the first thing the mesh tells this
// node would tear down the mesh (novox/hq 04-ISSUES/010).
//
// What it does not do is the host's own lifecycle bookkeeping — recording a known-good version,
// clearing the launcher's start counter. Those are facts about a running `mesh-host`, and this is
// not one.
func ApplyBundle(ctx context.Context, o Options, sys system.System, d *declaration.Declaration,
source string, run Runner, say func(string)) (apply.Report, error) {
// Refuse a shape this host cannot apply before anything is applied, exactly as `mesh-host`
// does: finding out half way through is the half-configured machine tier 0 exists to prevent.
if err := system.Check(sys, d); err != nil {
return apply.Report{}, err
}
known, err := store.Load(o.State)
if err != nil {
return apply.Report{}, err
}
report, updated, applyErr := apply.Apply(ctx, sys, d, known, store.OriginCarried, run,
func(line string) { say(" " + strings.TrimPrefix(line, " ")) }, refuseSealed)
// Saved whichever way it went, for the reason `mesh-host` gives: what was applied before a
// failure is on the machine either way, and a host that did not record it would believe it
// owns less than it does and leave that behind for ever.
if saveErr := store.Save(o.State, updated); saveErr != nil {
if applyErr != nil {
return report, fmt.Errorf("%w\n\nand this node's state could not be saved: %v",
applyErr, saveErr)
}
return report, saveErr
}
if applyErr != nil {
return report, fmt.Errorf("%w\n\nThe machine is in whatever state that left it. Fix what "+
"is named above and run this again — every step is idempotent, and the ones that "+
"already succeeded will say so", applyErr)
}
return report, nil
}
// refuseSealed is what happens when a bundle contains a file the mesh sealed to this node.
//
// It cannot happen and it is refused with a sentence rather than a nil dereference. A sealing key
// is generated at enrolment (`internal/identity`), and enrolment is something that happens on a
// mesh — which is the thing this program is raising. A substrate bundle carrying a sealed file
// would be a bundle written for a node that has already joined.
func refuseSealed(string) ([]byte, error) {
return nil, errors.New(
"this bundle contains a file sealed to a node's key, and a machine that has not enrolled " +
"has no such key. A substrate is applied before any mesh exists, so it can carry no " +
"secret the mesh sealed")
}
// WorkOutSystem decides which half of the host applies things on this machine, and proves it.
//
// `mesh-host` pins this at link time because it is built for one operating system and refuses to
// touch a machine without knowing which (novox/hq ADR 0005). An installer run by hand has no
// link-time to pin it at, so it asks — but it does not guess: every system already knows how to
// prove it is the one it claims to be, by asking its package database about a package that is
// certainly there. Exactly one may answer.
//
// A machine where none answers is refused with what each of them said, because "unsupported
// system" is a sentence nobody can act on and "pacman does not answer here" is.
func WorkOutSystem(ctx context.Context, run Runner, named string) (system.System, error) {
if strings.TrimSpace(named) != "" {
chosen, err := system.For(named)
if err != nil {
return nil, err
}
if err := chosen.Confirm(ctx, run); err != nil {
return nil, fmt.Errorf("--system %s was given, and this machine says otherwise: %w",
named, err)
}
return chosen, nil
}
var answered []system.System
var refusals []string
for _, candidate := range system.All() {
if err := candidate.Confirm(ctx, run); err != nil {
refusals = append(refusals, fmt.Sprintf(" %s: %v", candidate.Name(), err))
continue
}
answered = append(answered, candidate)
}
switch len(answered) {
case 1:
return answered[0], nil
case 0:
return nil, fmt.Errorf(
"this machine is none of the systems this installer knows how to change, so nothing "+
"was attempted:\n%s\nName one with --system if it is really one of them and its "+
"package database is merely unwell", strings.Join(refusals, "\n"))
default:
var names []string
for _, s := range answered {
names = append(names, s.Name())
}
return nil, fmt.Errorf(
"this machine answers as %s at once, and the installer must not choose between them: "+
"package names and unit names differ, and picking wrong misconfigures the machine "+
"quietly. Say which with --system", strings.Join(names, " and "))
}
}