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
183 lines
6.7 KiB
Go
183 lines
6.7 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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"
|
|
)
|
|
|
|
// `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)
|
|
}
|
|
}
|