Files
mesh-host/internal/bootstrap/apply.go
T
mesh-tools 52600ec7bd
mesh/merge-gate head 52600ec7bd61 merged into trunk cdcba9ca7375 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 52600ec7bd61 merged into trunk cdcba9ca7375 — pass: its merge-check.sh passed
mesh/delivery delivered
Take the node-engine's parser from mesh-sdk: remaining imports (issue 1392)
2026-10-20 11:17:14 +00:00

173 lines
7.9 KiB
Go

package bootstrap
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/declaration"
"github.com/novox/mesh-host/internal/apply"
"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 foundation, 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 foundation 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.
//
// What it does record is that the bundle was consumed, and in which mode the operator raised
// the machine. `raw` is the exact bytes of what is applied — the bundle as rewritten for this
// machine — and its digest is what `mesh-host reconcile` later holds the carried bundle against,
// by digest: a host built from the carried template does not match it, since genesis rewrote the
// ports, root credentials and adoption; a host built from the lock genesis wrote out does.
// Applying the unrewritten template on a raised node recreated the store and loaded the converged
// filter on an adopted one (novox/hq issue 104).
func ApplyBundle(ctx context.Context, o Options, sys system.System, d *declaration.Declaration,
raw []byte, 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
}
// One apply at a time on this machine: a host already running here applies too.
unlock, err := store.Lock(o.State, func() { say(" waiting another apply holds this node's state") })
if err != nil {
return apply.Report{}, err
}
defer unlock()
known, err := store.Load(o.State)
if err != nil {
return apply.Report{}, err
}
// The bundle writes over whatever the machine has at the paths the foundation needs — a
// distribution's own /etc/nftables.conf among them — so it keeps the original of each file it
// has no record of, beside the node's state, exactly as a declaration from the mesh does
// (novox/hq ADR 0100).
// Minding the maintenance windows a running host has open: the carried bundle is where the
// store's registry comes from, and an installer re-run at 03:31 must not restart it under its
// collector (novox/hq issue 224).
report, updated, applyErr := apply.ApplyMindingWindows(ctx, sys, d, known, store.OriginCarried, run,
func(line string) { say(" " + strings.TrimPrefix(line, " ")) }, refuseSealed,
apply.KeepIn(filepath.Dir(o.State)), apply.WindowsIn(filepath.Dir(o.State)))
// The bundle is consumed, whichever way the apply went: what is on the machine came from these
// bytes, and the carried ones must not be applied over it. The mode is the operator's word at
// genesis, and only at genesis: the controller records the same and says it in every
// declaration from then on (novox/hq ADR 0100), so a node the mesh has spoken to — this
// installer re-run on it, or finishing a pivot — keeps the mode it has, which may since have
// been flipped.
updated.Genesis = &store.Genesis{Digest: apply.DigestOf(raw), At: time.Now().UTC(), Rewritten: true}
if updated.Mode == "" {
updated.Mode = store.ModeConverged
if o.Adopted {
updated.Mode = store.ModeAdopted
}
}
// 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 foundation 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 foundation 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 "))
}
}