Files
mesh-host/internal/bootstrap/apply_test.go
jschoubben f08a8ea3f7 Refuse every file once the mesh has spoken, plan the cutover as one, and let the kept declaration repair the mode
Review of the fix for hq issue 104 found three faults in it. A file applied
on an enrolled node — the mesh's own last declaration included — is applied
as the bundle is, so its resources are recorded as the machine's own and
what the mesh declared reads as undeclared: the plan removed the foundation.
`apply FILE` is for a machine the mesh has not spoken to, and is now refused
saying so whenever declared.json exists. The plan looked at what is held
before what the declaration says is taken, so the one cutover ADR 0100 says
must be previewed read as a hold; it now decides in holdOnAdopted's order,
models a step run inside a held container, and a test holds the plan's
sequence to the apply's outcomes. Genesis wrote the mode on every run, so a
re-run after `converge` left the state saying adopted while the kept,
signed declaration said converged, and the reconcile loop refused every five
minutes with no delivery coming to end it: genesis now writes the mode only
when none is recorded, and where the state and the verified kept declaration
disagree, the kept declaration wins and the repair is said.

Also: a file lock beside the state, taken by the link service, the host's
own commands and the installer alike, so a `reconcile` run by hand no
longer races the loop's save — chosen over refusing while a named service is
active, which would miss a `mesh-host run` started by hand; `--json
--dry-run` emits {plan} like an apply emits {plan, report}; the README's
duplicate flag line; and the bundle refusal is about the digest, not a claim
the carried bytes can never match what genesis applied.
2026-09-23 23:35:49 +02:00

183 lines
6.7 KiB
Go

package bootstrap
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"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"
)
// `mesh-host` is built for one operating system and pins it at link time. An installer run by hand
// has no link time, so it asks — and it does not guess: each 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. Getting this wrong installs with the wrong package manager and the wrong unit names.
func TestTheMachineIsAskedWhichSystemItIs(t *testing.T) {
// Only pacman answers, so this is the arch host and nothing had to be told so.
onlyPacman := func(_ context.Context, name string, _ ...string) (string, error) {
if name == "pacman" {
return "pacman 7.0.0-1\n", nil
}
return "", errors.New("command not found")
}
chosen, err := WorkOutSystem(context.Background(), onlyPacman, "")
if err != nil {
t.Fatal(err)
}
if chosen.Name() != "arch" {
t.Errorf("this machine was worked out to be %q", chosen.Name())
}
}
// A machine that is none of them is refused with what each of them said. "Unsupported system" is
// a sentence nobody can act on; "pacman does not answer here" is.
func TestAMachineThatIsNoneOfThemIsRefusedWithWhatEachSaid(t *testing.T) {
nothing := func(context.Context, string, ...string) (string, error) {
return "", errors.New("command not found")
}
_, err := WorkOutSystem(context.Background(), nothing, "")
if err == nil {
t.Fatal("a machine that answers as no known system was accepted")
}
for _, wanted := range []string{"arch:", "alpine:", "--system"} {
if !strings.Contains(err.Error(), wanted) {
t.Errorf("the refusal does not mention %q:\n%v", wanted, err)
}
}
}
// And a machine that was TOLD what it is still has to prove it. Installing the arch half of the
// host on Alpine must say so once, at the start, rather than failing later inside pacman.
func TestASystemThatWasNamedIsStillProved(t *testing.T) {
onlyApk := func(_ context.Context, name string, _ ...string) (string, error) {
if name == "apk" {
return "apk-tools-2.14.0\n", nil
}
return "", errors.New("command not found")
}
if _, err := WorkOutSystem(context.Background(), onlyApk, "arch"); err == nil {
t.Fatal("--system arch was believed on a machine where pacman does not answer")
}
if _, err := WorkOutSystem(context.Background(), onlyApk, "alpine"); err != nil {
t.Errorf("--system alpine was refused on a machine where apk answers: %v", err)
}
}
func TestASystemNobodyHasBuiltIsRefusedByName(t *testing.T) {
anything := func(context.Context, string, ...string) (string, error) { return "", nil }
_, err := WorkOutSystem(context.Background(), anything, "debian")
if err == nil {
t.Fatal("--system debian was accepted, and no debian host is built")
}
if !strings.Contains(err.Error(), "arch") {
t.Errorf("the refusal does not say which systems exist: %v", err)
}
}
// A foundation is applied before any mesh exists, so it can carry no secret the mesh sealed — there
// is no key to open one with. Refused with a sentence rather than a nil dereference.
func TestASealedFileInAFoundationIsRefusedWithAReason(t *testing.T) {
_, err := refuseSealed("anything")
if err == nil {
t.Fatal("a sealed file in a foundation bundle was accepted")
}
if !strings.Contains(err.Error(), "has not enrolled") {
t.Errorf("the refusal does not say why there is no key: %v", err)
}
}
// Defends novox/hq ADR 0100: the carried bundle keeps the original of a file it writes over that
// the host has no record of — the distribution's own ruleset, say.
func TestTheBundleKeepsTheOriginalOfWhatItWritesOver(t *testing.T) {
dir := t.TempDir()
conf := filepath.Join(dir, "nftables.conf")
if err := os.WriteFile(conf, []byte("# the distribution's own\n"), 0o644); err != nil {
t.Fatal(err)
}
d, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[
{"id":"base-filter","type":"file","path":"` + conf + `","content":"table inet mesh {}\n"}]}`))
if err != nil {
t.Fatal(err)
}
sys, err := system.For("arch")
if err != nil {
t.Fatal(err)
}
o := Options{State: filepath.Join(dir, "state.json")}
report, err := ApplyBundle(context.Background(), o, sys, d, nil, nil, quietly)
if err != nil {
t.Fatal(err)
}
detail := report.Outcomes[0].Detail
at := strings.Index(detail, "kept at ")
if at < 0 {
t.Fatalf("the bundle wrote over a file it had no record of and kept nothing: %q", detail)
}
if got, err := os.ReadFile(detail[at+len("kept at "):]); err != nil ||
string(got) != "# the distribution's own\n" {
t.Errorf("the kept original is %q (%v)", got, err)
}
}
// Defends novox/hq issue 104: genesis consumes the bundle, recording the digest of what it applied
// and the mode the operator raised the machine in, so the host's own `reconcile` never applies the
// carried bytes over it.
func TestGenesisConsumesTheBundleAndRecordsTheMode(t *testing.T) {
dir := t.TempDir()
raw := []byte(`{"declaration":1,"resources":[
{"id":"a","type":"file","path":"` + filepath.Join(dir, "a.conf") + `","content":"x\n"}]}`)
d, err := declaration.ParseFileTrusted(raw)
if err != nil {
t.Fatal(err)
}
sys, err := system.For("arch")
if err != nil {
t.Fatal(err)
}
for _, adopted := range []bool{false, true} {
o := Options{State: filepath.Join(dir, fmt.Sprintf("state-%v.json", adopted)), Adopted: adopted}
if _, err := ApplyBundle(context.Background(), o, sys, d, raw, nil, quietly); err != nil {
t.Fatal(err)
}
known, err := store.Load(o.State)
if err != nil {
t.Fatal(err)
}
if known.Genesis == nil || known.Genesis.Digest != apply.DigestOf(raw) || !known.Genesis.Rewritten {
t.Errorf("adopted=%v: genesis did not record the bundle it consumed: %+v", adopted, known.Genesis)
}
want := store.ModeConverged
if adopted {
want = store.ModeAdopted
}
if known.Mode != want {
t.Errorf("adopted=%v: genesis recorded the mode as %q, want %q", adopted, known.Mode, want)
}
}
// Re-run on a node the mesh has spoken to since — and converged — genesis leaves the mode
// alone: it is the operator's word at genesis, and the controller's from then on.
o := Options{State: filepath.Join(dir, "state-flipped.json"), Adopted: true}
if err := store.Save(o.State, store.State{Mode: store.ModeConverged}); err != nil {
t.Fatal(err)
}
if _, err := ApplyBundle(context.Background(), o, sys, d, raw, nil, quietly); err != nil {
t.Fatal(err)
}
if known, _ := store.Load(o.State); known.Mode != store.ModeConverged {
t.Errorf("a genesis re-run set the mode back to %q over the mesh's converged", known.Mode)
}
}