An operator ran `mesh-host reconcile` on an adopted control-node with twelve modules assigned. It applied the bundle the host carries — the genesis declaration, foundation only, converged: recreated the store, failed on the broker's held port, wrote the converged base filter and started its service, and stopped at the first failing action. The filter closed the machine for forty-five minutes. The host reported the node adopted in every report, the declaration said converged, and nothing compared the two; nothing was printed before acting (hq issue 104). The host now records the node's mode — from every declaration the mesh sends, and at genesis from what the operator said — and refuses, at the point of application, a declaration that says the other mode, naming both and the act that changes it. Only a declaration the link delivers, signed, changes the mode: that is how `converge` and `adopt` arrive, so the flip still works and nothing else can do it. Genesis marks the bundle consumed, with the digest of what it applied, so `reconcile` holds a node the mesh has spoken to against what the mesh last said and never the bundle, and refuses the carried bytes when they are not what genesis applied. A file is refused when it is not what the mesh last said: a declaration carries no sequence and no issued-at, so the host cannot tell older from newer, and says so. Both commands print what they would change — a hold, a removal, an action named as one — before touching anything, and --dry-run is that list and nothing more.
170 lines
6.1 KiB
Go
170 lines
6.1 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)
|
|
}
|
|
}
|
|
}
|