Files
mesh-host/internal/bootstrap/apply.go
T

141 lines
6.0 KiB
Go

package bootstrap
import (
"context"
"errors"
"fmt"
"path/filepath"
"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 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.
func ApplyBundle(ctx context.Context, o Options, sys system.System, d *declaration.Declaration,
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
}
// 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).
report, updated, applyErr := apply.ApplyKeeping(ctx, sys, d, known, store.OriginCarried, run,
func(line string) { say(" " + strings.TrimPrefix(line, " ")) }, refuseSealed,
apply.KeepIn(filepath.Dir(o.State)))
// 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 "))
}
}