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
173 lines
7.9 KiB
Go
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 "))
|
|
}
|
|
}
|