141 lines
6.0 KiB
Go
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 "))
|
|
}
|
|
}
|