Files
mesh-host/internal/apply/apply_test.go
T
jochen 3112c881e4 Undeclaring gives a unit back the state it was found in, and removes a process the mesh made (hq ADR 0118, issue 130)
A service undeclared used to be stopped: unassigning the private network stopped the container
runtime, unassigning sshd would stop ssh, an uplink module would take the machine offline. The
host now records the unit's state when it first applies it and restores that on undeclare —
found running stays running; started by the mesh (the converge filter) is stopped again; nothing
is started on the way out; a pre-existing record leaves the unit alone.

An undeclared process had no removal at all and failed every apply on its node; its unit, timer
and bundle are now removed.
2026-09-27 00:21:25 +02:00

1688 lines
64 KiB
Go

package apply
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/store"
"github.com/novox/mesh-host/internal/system"
)
// Each test names the decision it defends (novox/hq ADR 0017).
func parse(t *testing.T, raw string) *declaration.Declaration {
t.Helper()
d, err := declaration.Parse([]byte(raw))
if err != nil {
t.Fatalf("fixture is not a valid declaration: %v", err)
}
return d
}
// noServices refuses to run anything. Used where a test declares no services, so that a test
// which accidentally reaches the service manager fails loudly instead of passing quietly.
func noServices(context.Context, string, ...string) (string, error) {
return "", errors.New("this test declares no services and should not have run a command")
}
func TestApplyingTwiceChangesNothingTheSecondTime(t *testing.T) {
// Idempotence is what makes an apply safe to run on a schedule. Without it, a host that
// reconciles every few minutes rewrites files forever and every reader sees churn.
dir := t.TempDir()
d := parse(t, `{"declaration":1,"resources":[
{"id":"d","type":"directory","path":"`+dir+`/etc","mode":"0755"},
{"id":"f","type":"file","path":"`+dir+`/etc/a.conf","content":"hello\n","mode":"0640"}
]}`)
first, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if !first.Changed() {
t.Fatal("the first apply on an empty machine changed nothing")
}
second, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if second.Changed() {
t.Errorf("the second apply changed something: %+v", second.Outcomes)
}
}
// Defends novox/hq ADR 0011: a managed file is generated onto a node and never edited there.
//
// Not by overwriting silently — by noticing. An edit that vanishes without a word is how somebody
// spends an afternoon re-fixing a bug they already fixed.
func TestADriftedMachineIsReturned(t *testing.T) {
// The other half of idempotence, and the half that matters: converging is not "do nothing
// if the state file says it was done". The machine is read, not the record.
dir := t.TempDir()
path := filepath.Join(dir, "a.conf")
d := parse(t, `{"declaration":1,"resources":[
{"id":"f","type":"file","path":"`+path+`","content":"correct\n","mode":"0644"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("someone edited this\n"), 0o644); err != nil {
t.Fatal(err)
}
report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if !report.Changed() {
t.Fatal("a drifted file was left drifted")
}
got, _ := os.ReadFile(path)
if string(got) != "correct\n" {
t.Errorf("the file was not returned: %q", got)
}
}
func TestADroppedResourceIsRemoved(t *testing.T) {
// novox/hq ADR 0005: the host removes what it previously applied and is no longer
// declared. Removing a line from a declaration is an act with an effect.
dir := t.TempDir()
keep := filepath.Join(dir, "keep.conf")
drop := filepath.Join(dir, "drop.conf")
both := parse(t, `{"declaration":1,"resources":[
{"id":"keep","type":"file","path":"`+keep+`","content":"a\n"},
{"id":"drop","type":"file","path":"`+drop+`","content":"b\n"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), both, store.State{}, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
one := parse(t, `{"declaration":1,"resources":[
{"id":"keep","type":"file","path":"`+keep+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), one, state, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(drop); !errors.Is(err, os.ErrNotExist) {
t.Error("a resource dropped from the declaration was left on the machine")
}
if _, err := os.Stat(keep); err != nil {
t.Error("a declared resource was removed")
}
if _, still := state.Find("drop"); still {
t.Error("the host still believes it owns what it removed")
}
if report.Outcomes[0].Action != "removed" {
t.Errorf("removal is not reported first: %+v", report.Outcomes)
}
}
func TestNothingTheHostDidNotCreateIsTouched(t *testing.T) {
// The boundary the whole removal rule turns on. A machine has things on it the mesh did
// not put there, and a converger that treats "not declared" as "must not exist" deletes
// them. Authoritative over its own footprint; inert everywhere else.
dir := t.TempDir()
stranger := filepath.Join(dir, "not-ours.conf")
if err := os.WriteFile(stranger, []byte("someone else's\n"), 0o644); err != nil {
t.Fatal(err)
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"ours","type":"file","path":"`+filepath.Join(dir, "ours.conf")+`","content":"a\n"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(stranger)
if err != nil || string(got) != "someone else's\n" {
t.Error("a file the host did not create was removed or changed")
}
}
func TestARenameToTheSamePathDoesNotDeleteTheNewFile(t *testing.T) {
// Why removal happens FIRST. A resource leaving a declaration while another arrives at the
// same path is an ordinary rename; removing afterwards would delete the file just written.
dir := t.TempDir()
path := filepath.Join(dir, "shared.conf")
before := parse(t, `{"declaration":1,"resources":[
{"id":"old","type":"file","path":"`+path+`","content":"old\n"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), before, store.State{}, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
after := parse(t, `{"declaration":1,"resources":[
{"id":"new","type":"file","path":"`+path+`","content":"new\n"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), after, state, store.OriginCarried, noServices, nil, nil); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("the renamed resource is gone: %v", err)
}
if string(got) != "new\n" {
t.Errorf("content is %q, want the new one", got)
}
}
func TestAFailedStepFailsTheApplyAndTheRestIsStillAttempted(t *testing.T) {
// The apply fails, names the resource, and carries what did happen — because the machine is
// in whatever state the apply reached and the only honest thing to hand back is that list.
//
// **And everything is attempted.** It used to stop at the first failure, which made one
// broken resource hold the whole machine hostage: a module declaring a package that does not
// exist meant every module after it was never applied, for ever
// (novox/hq 04-ISSUES/011). The case for stopping was that a later resource may depend on an
// earlier one — and it still may, and it then fails its own check and is reported, which is
// more information than not attempting it.
dir := t.TempDir()
blocker := filepath.Join(dir, "blocker")
if err := os.WriteFile(blocker, []byte("i am a file\n"), 0o644); err != nil {
t.Fatal(err)
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"fine","type":"file","path":"`+filepath.Join(dir, "fine.conf")+`","content":"a\n"},
{"id":"doomed","type":"directory","path":"`+blocker+`"},
{"id":"after","type":"file","path":"`+filepath.Join(dir, "after.conf")+`","content":"b\n"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil)
if err == nil {
t.Fatal("an impossible resource did not fail the apply")
}
var applyErr *Error
if !errors.As(err, &applyErr) {
t.Fatalf("expected an apply error, got %T", err)
}
if applyErr.Resource != "doomed" {
t.Errorf("the failure names %q, not the resource that failed", applyErr.Resource)
}
// Everything that worked is in the report, before and after the failure.
if len(applyErr.Done.Outcomes) != 2 {
t.Errorf("the error does not carry what was applied: %+v", applyErr.Done.Outcomes)
}
if _, err := os.Stat(filepath.Join(dir, "after.conf")); err != nil {
t.Error("a resource after the failing one was never attempted, so one broken module " +
"still blocks every module after it")
}
}
func TestEveryFailureIsCountedNotJustTheFirst(t *testing.T) {
// "One thing failed" and "eleven things failed" are different machines, and the first line is
// what somebody reads.
dir := t.TempDir()
for _, name := range []string{"one", "two"} {
if err := os.WriteFile(filepath.Join(dir, name), []byte("a file\n"), 0o644); err != nil {
t.Fatal(err)
}
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"first","type":"directory","path":"`+filepath.Join(dir, "one")+`"},
{"id":"second","type":"directory","path":"`+filepath.Join(dir, "two")+`"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil)
if err == nil {
t.Fatal("two impossible resources did not fail the apply")
}
var applyErr *Error
if !errors.As(err, &applyErr) {
t.Fatalf("got %T", err)
}
if applyErr.Others != 1 {
t.Errorf("the failure says %d others also failed, and one did", applyErr.Others)
}
if !strings.Contains(err.Error(), "one other resource also failed") {
t.Errorf("the message does not say others failed: %v", err)
}
}
func TestNothingIsRecordedUntilItWorked(t *testing.T) {
// novox/hq ADR 0018. A record written before the fact restates the request in a new place
// and inherits none of the authority of having happened.
dir := t.TempDir()
blocker := filepath.Join(dir, "blocker")
if err := os.WriteFile(blocker, []byte("x\n"), 0o644); err != nil {
t.Fatal(err)
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"doomed","type":"directory","path":"`+blocker+`"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil)
if err == nil {
t.Fatal("expected a failure")
}
if _, claimed := state.Find("doomed"); claimed {
t.Error("the host recorded owning something it failed to apply")
}
}
func TestAModeIsMaintainedNotJustSet(t *testing.T) {
// A permission set at creation is not a permission maintained — this repository has
// already paid for that once, with generated files left world-readable because the mode
// applied only when the file was first written.
dir := t.TempDir()
path := filepath.Join(dir, "secret.conf")
d := parse(t, `{"declaration":1,"resources":[
{"id":"f","type":"file","path":"`+path+`","content":"s\n","mode":"0600"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := os.Chmod(path, 0o666); err != nil {
t.Fatal(err)
}
report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
info, _ := os.Stat(path)
if info.Mode().Perm() != 0o600 {
t.Errorf("mode is %o after reconciling, want 0600", info.Mode().Perm())
}
if !report.Changed() {
t.Error("a mode that had drifted was reported as unchanged")
}
}
func TestAServiceIsReadBackNotAssumed(t *testing.T) {
// `systemctl start` returning zero says the transaction was accepted, not that the unit is
// running. A unit that starts and immediately dies satisfies the command.
started := false
run := func(ctx context.Context, name string, args ...string) (string, error) {
if args[0] == "show" {
if started {
return "LoadState=loaded\nActiveState=failed\n", nil // started, then died
}
return "LoadState=loaded\nActiveState=inactive\n", nil
}
started = true
return "", nil // `systemctl start` succeeds
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"doomed.service","state":"running"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err == nil {
t.Fatal("a service that died immediately was reported as running")
}
if !strings.Contains(err.Error(), "asked to be running and is stopped") {
t.Errorf("the failure does not say what was observed: %v", err)
}
}
func TestAnUnknownServiceStateIsRefusedNotGuessed(t *testing.T) {
run := func(ctx context.Context, name string, args ...string) (string, error) {
return "LoadState=loaded\nActiveState=reticent\n", nil
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"odd.service","state":"running"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err == nil || !strings.Contains(err.Error(), "neither running nor stopped") {
t.Errorf("an unrecognised service state was not refused: %v", err)
}
}
func TestADroppedServiceIsGivenBackTheStateItWasFoundIn(t *testing.T) {
// novox/hq ADR 0118: undeclaring removes what the mesh made, gives back what it changed, and
// leaves what was the machine's. A service resource never installs a unit — so undeclaring it
// undoes what the mesh did to the unit, and nothing more. Stopping every undeclared unit is
// how unassigning the private network stopped the container runtime (novox/hq issue 130).
cases := []struct {
name string
found *store.FoundUnit
action string
stop bool
disable bool
}{
{"recorded before the host kept what it found", nil, "forgotten", false, false},
{"running before the mesh", &store.FoundUnit{State: "running"}, "forgotten", false, false},
{"running and enabled before the mesh", &store.FoundUnit{State: "running", Boot: "enabled"}, "forgotten", false, false},
{"started by the mesh", &store.FoundUnit{State: "stopped"}, "restored", true, false},
{"started and enabled by the mesh", &store.FoundUnit{State: "stopped", Boot: "disabled"}, "restored", true, true},
{"enabled by the mesh, running before it", &store.FoundUnit{State: "running", Boot: "disabled"}, "restored", false, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var commands []string
run := func(ctx context.Context, name string, args ...string) (string, error) {
commands = append(commands, strings.Join(args, " "))
if args[0] == "show" {
return "LoadState=loaded\nActiveState=active\n", nil
}
return "", nil
}
state := store.State{Resources: []store.Applied{
{ID: "s", Type: "service", Target: "unit.service", Found: c.found},
}}
d := parse(t, `{"declaration":1,"resources":[
{"id":"other","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
report, after, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(commands, "; ")
if stopped := strings.Contains(joined, "stop unit.service"); stopped != c.stop {
t.Errorf("stopped %v, want %v: %s", stopped, c.stop, joined)
}
if disabled := strings.Contains(joined, "disable unit.service"); disabled != c.disable {
t.Errorf("disabled %v, want %v: %s", disabled, c.disable, joined)
}
if strings.Contains(joined, "start unit.service") || strings.Contains(joined, "mask") {
t.Errorf("the host started or masked a unit on its way out: %s", joined)
}
if o := outcomeOf(report, "s"); o.Action != c.action {
t.Errorf("outcome %+v, want %s", o, c.action)
}
if _, still := after.Find("s"); still {
t.Error("the host still believes it owns the undeclared service")
}
})
}
}
func TestAServiceRecordsTheStateItWasFoundInOnceAndCarriesIt(t *testing.T) {
// Read the first time the host applies the unit — before it starts or enables anything —
// and never again: by the next apply, the unit's state is the mesh's doing.
active := "inactive"
enabled := "disabled"
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "show":
return "LoadState=loaded\nActiveState=" + active + "\n", nil
case "is-enabled":
return enabled, nil
case "start":
active = "active"
case "enable":
enabled = "enabled"
}
return "", nil
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"filter.service","state":"running","boot":"enabled"}
]}`)
_, first, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatal(err)
}
rec, _ := first.Find("s")
if rec.Found == nil || rec.Found.State != "stopped" || rec.Found.Boot != "disabled" {
t.Fatalf("first apply recorded %+v, want stopped and disabled", rec.Found)
}
_, second, err := Apply(context.Background(), archHost(t), d, first, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatal(err)
}
rec, _ = second.Find("s")
if rec.Found == nil || rec.Found.State != "stopped" {
t.Errorf("a later apply replaced what was found with what the mesh made: %+v", rec.Found)
}
// A record from before the host kept what it found is not given one later.
old := store.State{Resources: []store.Applied{{ID: "s", Type: "service", Target: "filter.service"}}}
_, third, err := Apply(context.Background(), archHost(t), d, old, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatal(err)
}
if rec, _ = third.Find("s"); rec.Found != nil {
t.Errorf("a record that predates the field was given one after the mesh had acted: %+v", rec.Found)
}
}
func TestAUnitThatDoesNotExistIsNotStopped(t *testing.T) {
// Found by applying inside a raised machine. `systemctl is-active` says "inactive" for a
// unit that DOES NOT EXIST exactly as it does for one that is installed and stopped, so
// declaring a unit stopped reported success for a unit the host cannot manage at all.
//
// Absence read as satisfaction — 04-ISSUES/007 wearing a different hat, and the mirror of
// the degraded-init bug the capability detector had.
absent := func(ctx context.Context, name string, args ...string) (string, error) {
return "LoadState=not-found\nActiveState=inactive\n", nil
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"never-installed.service","state":"stopped"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, absent, nil, nil)
if err == nil {
t.Fatal("a unit that does not exist was reported as satisfactorily stopped")
}
if !strings.Contains(err.Error(), "does not exist on this machine") {
t.Errorf("the failure does not say the unit is absent: %v", err)
}
if _, claimed := state.Find("s"); claimed {
t.Error("the host recorded owning a unit that is not installed")
}
}
func TestAMaskedUnitIsRefused(t *testing.T) {
// Masked means someone deliberately made it unstartable. Applying over that would undo a
// decision the host did not make and cannot see the reason for.
masked := func(ctx context.Context, name string, args ...string) (string, error) {
return "LoadState=masked\nActiveState=inactive\n", nil
}
d := parse(t, `{"declaration":1,"resources":[
{"id":"s","type":"service","unit":"masked.service","state":"running"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, masked, nil, nil); err == nil {
t.Fatal("a masked unit was accepted")
}
}
func TestForgettingAUnitThatIsGoneDoesNotStrandTheNode(t *testing.T) {
// Found on a real machine. Removing an orphaned service runs `systemctl stop`, which fails
// when the unit no longer exists — and a failure there fails the whole apply. A host
// holding a record of an uninstalled unit could then apply NOTHING, ever, with no way out
// but editing its state by hand.
//
// Removal is idempotent for the same reason os.RemoveAll is: the desired end state is
// already true.
var stopped bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if args[0] == "show" {
return "LoadState=not-found\nActiveState=inactive\n", nil
}
stopped = true
return "", errors.New("systemctl exited 5: Unit not loaded")
}
known := store.State{Resources: []store.Applied{
{ID: "gone", Type: "service", Target: "uninstalled.service"},
}}
d := parse(t, `{"declaration":1,"resources":[
{"id":"f","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), d, known, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("a vanished unit stranded the apply: %v", err)
}
if stopped {
t.Error("the host tried to stop a unit that does not exist")
}
if _, still := state.Find("gone"); still {
t.Error("the host still believes it owns a unit that is gone")
}
// "forgotten", not "removed": the host stopped believing it owns the unit, and did not
// remove anything, because there was nothing there to remove. Reporting an effect it did
// not have would be the same class of untruth as reporting a package uninstalled.
if report.Outcomes[0].Action != "forgotten" {
t.Errorf("the vanished unit was not reported as forgotten: %+v", report.Outcomes)
}
}
// --- package, container and action (novox/hq 07-the-foundation.md, ADR 0006, ADR 0005) ---
func parseTrusted(t *testing.T, raw string) *declaration.Declaration {
t.Helper()
d, err := declaration.ParseTrusted([]byte(raw))
if err != nil {
t.Fatalf("fixture is not a valid declaration: %v", err)
}
return d
}
const pinned = "docker.io/library/postgres@sha256:" +
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) {
// The same trap serviceState documents. `pacman -Q x` exits non-zero both for a package
// that is not installed and for a database that cannot be read — so believing the first
// answer would silently reinstall on a machine whose package manager is broken, or report
// "installed nothing" as success. The apply must fail instead.
run := func(ctx context.Context, name string, args ...string) (string, error) {
return "", errors.New("pacman: error: could not lock database")
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"package","package":"docker"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err == nil {
t.Fatal("a broken package database was read as 'not installed'")
}
if !strings.Contains(err.Error(), "does not answer") {
t.Errorf("failed for the wrong reason: %v", err)
}
}
func TestAnInstalledPackageIsNotReinstalled(t *testing.T) {
var installed bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if args[0] == "-S" {
installed = true
}
return "docker 27.0-1\n", nil // -Q succeeds for everything
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"package","package":"docker"}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if installed {
t.Error("a package that was already present was installed again")
}
if report.Changed() {
t.Errorf("an already-installed package reported a change: %+v", report.Outcomes)
}
}
func TestAPackageIsNeverUninstalled(t *testing.T) {
// Deliberate: the host cannot know what else needs the package. Uninstalling a container
// runtime because a declaration changed would stop every container on the node, and the
// machine may have had it before the mesh ever saw it. Undeclaring is not "remove it".
var uninstalled bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if len(args) > 0 && (args[0] == "-R" || args[0] == "-Rs") {
uninstalled = true
}
return "", nil
}
known := store.State{Resources: []store.Applied{
{ID: "rt", Type: "package", Target: "docker"},
}}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"f","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
]}`)
report, state, err := Apply(context.Background(), archHost(t), d, known, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("dropping a package stranded the apply: %v", err)
}
if uninstalled {
t.Fatal("the host uninstalled a package")
}
if _, still := state.Find("rt"); still {
t.Error("the host still believes it owns the package")
}
// "forgotten", not "removed" — the host must not claim an effect it declined to have.
if report.Outcomes[0].Action != "forgotten" {
t.Errorf("dropping a package was not reported as forgotten: %+v", report.Outcomes[0])
}
}
func TestAnActionThatIsAlreadyTrueDoesNotRun(t *testing.T) {
// Verify is the idempotency check as well as the read-back. The host does not know what a
// database is, so "is it already there" is a question only the declaration can ask.
var ran bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "create-db" {
ran = true
}
return "", nil // verify passes
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"db","type":"action","command":["create-db","mesh"],"verify":["has-db","mesh"]}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if ran {
t.Error("an action whose verify already passed was run anyway")
}
if report.Changed() {
t.Errorf("an already-satisfied action reported a change: %+v", report.Outcomes)
}
}
func TestAnActionThatSucceedsAndDoesNothingFails(t *testing.T) {
// The whole reason verify is mandatory: a command that exits zero and has no effect is
// this repository's most expensive failure shape. Here the command "succeeds" every time
// and verify never passes.
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "has-db" {
return "", errors.New("no such database")
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"db","type":"action","command":["create-db","mesh"],"verify":["has-db","mesh"]}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err == nil {
t.Fatal("an action that reported success and did nothing was accepted")
}
if !strings.Contains(err.Error(), "verify still fails") {
t.Errorf("failed for the wrong reason: %v", err)
}
if _, recorded := state.Find("db"); recorded {
t.Error("an action that did not work was recorded as applied")
}
}
func TestAnActionRunsInsideTheContainerItNames(t *testing.T) {
// Steps 2 and 3 of the bootstrap act on something inside the store's container, before
// there is any mesh to ask.
var sawExec bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "docker" && args[0] == "exec" && args[1] == "store" {
sawExec = true
return "", nil
}
return "", errors.New("not run in the container")
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"db","type":"action","in":"store","command":["createdb","mesh"],"verify":["psql","-lqt"]}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil); err != nil {
t.Fatalf("apply failed: %v", err)
}
if !sawExec {
t.Error("an action naming a container did not run inside it")
}
}
func TestAContainerThatExitsImmediatelyFailsTheApply(t *testing.T) {
// `docker run --detach` returning an id says the container was created, not that it is
// still running. A container whose entrypoint dies satisfies the command exactly as one
// that came up does — which is the read-back rule, in the place it matters most.
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch {
case args[0] == "info":
return "27.0\n", nil
case args[0] == "container":
return "false\t" + "", nil // exists, not running
case args[0] == "run":
return "deadbeef\n", nil
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`"}
]}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err == nil {
t.Fatal("a container that exited immediately was reported as applied")
}
if !strings.Contains(err.Error(), "is not running") {
t.Errorf("failed for the wrong reason: %v", err)
}
if _, recorded := state.Find("store"); recorded {
t.Error("a container that is not running was recorded as applied")
}
}
func TestAContainerWhoseDeclarationChangedIsReplaced(t *testing.T) {
// A container's configuration is fixed when it is created, so any change is a replacement.
// The spec label is what makes the difference visible without diffing everything the
// runtime reports — which cannot be done reliably, because a runtime normalises what it is
// given and that is indistinguishable from drift.
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
]}`)
want := containerSpec(d.Resources[0].(*declaration.Container), inputs{})
var removed, created bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "info":
return "27.0\n", nil
case "container":
if created {
return "true\t" + want, nil
}
return "true\tsome-older-spec", nil
case "rm":
removed = true
return "", nil
case "run":
created = true
return "deadbeef\n", nil
}
return "", nil
}
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if !removed || !created {
t.Fatalf("a changed container was not replaced (removed=%v created=%v)", removed, created)
}
if report.Outcomes[0].Action != "updated" {
t.Errorf("a replacement was not reported as an update: %+v", report.Outcomes[0])
}
}
func TestAContainerThatMatchesIsLeftAlone(t *testing.T) {
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`","env":{"PGDATA":"/data"}}
]}`)
spec := containerSpec(d.Resources[0].(*declaration.Container), inputs{})
var touched bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "info":
return "27.0\n", nil
case "container":
return "true\t" + spec, nil
}
touched = true
return "", nil
}
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if touched {
t.Error("a container that already matched was restarted")
}
if report.Changed() {
t.Errorf("a matching container reported a change: %+v", report.Outcomes)
}
}
func TestAContainerIsRecreatedWhenARestartOnResourceChanged(t *testing.T) {
// A container reads a mounted file once, at start. When the file changed this pass but the
// container's spec did not, the plain "spec matches, leave it" rule would keep the process
// holding the old value for ever, with every check passing (novox/hq 04-ISSUES/009). A
// container names the resources it must reflect in restart-on, the same as a service, and the
// host recreates it. Here the config file is fresh, so it is written this pass, and the
// already-running-and-matching container must still be replaced.
dir := t.TempDir()
conf := filepath.Join(dir, "config.json")
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"config","type":"file","path":"`+conf+`","content":"{\"token\":\"new\"}\n"},
{"id":"app","type":"container","name":"app","image":"`+pinned+`","restart-on":["config"]}
]}`)
// The spec the container was created with, back when the file said something else. Built the
// way the host builds it, so this is the real comparison rather than a hand-written string:
// what a container reads is part of what it is, so the old content yields a different spec.
was := map[string]string{"config": declaredDigest(&declaration.File{Content: "{\"token\":\"old\"}\n"})}
now := map[string]string{"config": declaredDigest(d.Resources[0].(*declaration.File))}
stale := containerSpec(d.Resources[1].(*declaration.Container), inputs{declares: was})
fresh := containerSpec(d.Resources[1].(*declaration.Container), inputs{declares: now})
var removed, created bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "info":
return "27.0\n", nil
case "container":
// Already there and running, created against the file as it was. After the host
// recreates it, the runtime holds the one it just made — as a real one would.
if created {
return "true\t" + fresh, nil
}
return "true\t" + stale, nil
case "rm":
removed = true
return "", nil
case "run":
created = true
return "deadbeef\n", nil
}
return "", nil
}
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if !removed || !created {
t.Fatalf("a container was not recreated when its restart-on file changed (removed=%v created=%v)", removed, created)
}
app := report.Outcomes[len(report.Outcomes)-1]
if app.Action != "updated" || !strings.Contains(app.Detail, "config") {
t.Errorf("the recreation did not name why it happened: %+v", app)
}
}
func TestRestartOnFiresOnlyForResourcesThatChanged(t *testing.T) {
// restart-on must not mean "always restart": it names resources, and only a resource that
// changed this pass is a reason. This is the guard shared by containers and services
// (novox/hq 04-ISSUES/009), so a container that reflects an unchanged file is left running.
changed := map[string]bool{"other": true}
if got := restartedBy([]string{"config"}, changed); len(got) != 0 {
t.Errorf("an unchanged resource was treated as a reason to restart: %v", got)
}
changed["config"] = true
if got := restartedBy([]string{"config", "missing"}, changed); len(got) != 1 || got[0] != "config" {
t.Errorf("restart-on did not name exactly the changed resource: %v", got)
}
}
// --- boot state (novox/hq: a unit started but not enabled stops being true at the next reboot) ---
// systemctlStub answers `show` and `is-enabled` the way systemd does, and records the verbs it
// was asked to perform. Real command shapes, because the trap being tested is what systemd
// actually says rather than what a fake would.
func systemctlStub(t *testing.T, load, active, enabled string, verbs *[]string) Runner {
t.Helper()
return func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "show":
return "LoadState=" + load + "\nActiveState=" + active + "\n", nil
case "is-enabled":
// Non-zero for everything but "enabled" — the exit code says nothing useful, which
// is the whole reason this reads the output.
if enabled == "enabled" {
return enabled + "\n", nil
}
return enabled + "\n", errors.New("exit status 1")
case "enable":
*verbs = append(*verbs, "enable")
enabled = "enabled"
return "", nil
case "disable":
*verbs = append(*verbs, "disable")
enabled = "disabled"
return "", nil
case "start":
*verbs = append(*verbs, "start")
active = "active"
return "", nil
case "stop":
*verbs = append(*verbs, "stop")
active = "inactive"
return "", nil
}
return "", nil
}
}
func TestAServiceIsEnabledAtBootWhenAsked(t *testing.T) {
// The gap this closes: the host could start a unit and never make it survive a reboot, so
// the declaration reported success and stopped being true at the next power cut.
var verbs []string
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"docker.service","state":"running","boot":"enabled"}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "inactive", "disabled", &verbs), nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if len(verbs) != 2 || verbs[0] != "enable" || verbs[1] != "start" {
t.Errorf("expected enable then start, got %v", verbs)
}
if report.Outcomes[0].Action != "updated" {
t.Errorf("enabling and starting was not reported as an update: %+v", report.Outcomes[0])
}
}
func TestBootIsEnabledBeforeTheUnitIsStarted(t *testing.T) {
// Order matters when an apply fails part way. Enabled-and-stopped comes back at the next
// boot; running-and-disabled does not. So the more durable half is made true first.
var verbs []string
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"docker.service","state":"running","boot":"enabled"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "inactive", "disabled", &verbs), nil, nil); err != nil {
t.Fatal(err)
}
if len(verbs) < 2 || verbs[0] != "enable" {
t.Errorf("boot state was not made true first: %v", verbs)
}
}
func TestAlreadyEnabledAndRunningIsUnchanged(t *testing.T) {
var verbs []string
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"docker.service","state":"running","boot":"enabled"}
]}`)
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "active", "enabled", &verbs), nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if len(verbs) != 0 {
t.Errorf("a unit already in the declared state was touched: %v", verbs)
}
if report.Changed() {
t.Errorf("an unchanged service reported a change: %+v", report.Outcomes)
}
}
func TestOmittingBootLeavesItAlone(t *testing.T) {
// Absent means the host asserts nothing. A machine whose operator enabled something must
// not have it silently disabled because a declaration did not mention it.
var verbs []string
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"docker.service","state":"running"}
]}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "inactive", "enabled", &verbs), nil, nil); err != nil {
t.Fatal(err)
}
for _, v := range verbs {
if v == "enable" || v == "disable" {
t.Errorf("boot state was changed by a declaration that did not mention it: %v", verbs)
}
}
}
func TestAStaticUnitCannotBeEnabled(t *testing.T) {
// `static` is neither enabled nor disabled: the unit has no install section and CANNOT be
// enabled. Reading it as "disabled" would have the host try, fail, and blame the wrong
// thing — the same shape as reading a missing unit as "stopped".
var verbs []string
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"dbus.socket","state":"running","boot":"enabled"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "active", "static", &verbs), nil, nil)
if err == nil {
t.Fatal("a static unit was accepted as enable-able")
}
if !strings.Contains(err.Error(), "no install section") {
t.Errorf("failed for the wrong reason: %v", err)
}
}
func TestAnUnknownBootStateIsRefusedNotGuessed(t *testing.T) {
var verbs []string
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"rt","type":"service","unit":"x.service","state":"running","boot":"enabled"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
systemctlStub(t, "loaded", "active", "indirect", &verbs), nil, nil)
if err == nil {
t.Fatal("an unrecognised boot state was guessed at instead of refused")
}
}
// --- more than one container runtime (novox/hq ADR 0005) ---
func TestTheRuntimeProbeIsPerRuntime(t *testing.T) {
// Verified against a real podman 6.1.0 before this was written:
//
// docker info --format '{{.ServerVersion}}' -> 29.7.2
// podman info --format '{{.ServerVersion}}' -> Error: can't evaluate field
// ServerVersion in type system.infoReport
// podman info --format '{{.Version.Version}}' -> 6.1.0
//
// So a single probe cannot find both, and a host that used docker's would report a machine
// with podman as having no container runtime at all.
for _, tc := range []struct {
name, present, wantProbe string
}{
{"docker", "docker", "{{.ServerVersion}}"},
{"podman", "podman", "{{.Version.Version}}"},
} {
t.Run(tc.name, func(t *testing.T) {
var probedWith string
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name != tc.present {
return "", errors.New("not installed")
}
if args[0] == "info" {
probedWith = args[2]
}
return "ok\n", nil
}
got, err := containerRuntime(context.Background(), run)
if err != nil {
t.Fatalf("%s was present and was not found: %v", tc.present, err)
}
if got != tc.present {
t.Errorf("found %q, expected %q", got, tc.present)
}
if probedWith != tc.wantProbe {
t.Errorf("probed %s with %q; that template does not work on it",
tc.present, probedWith)
}
})
}
}
func TestAContainerUsesTheRuntimeTheMachineHas(t *testing.T) {
// The applier must not call `docker` on a machine that has podman. Adoption keeps what the
// machine already has (novox/hq research 012), so hardcoding one contradicts it.
var calledWith []string
run := func(ctx context.Context, name string, args ...string) (string, error) {
if name == "docker" {
return "", errors.New("not installed")
}
calledWith = append(calledWith, name)
switch args[0] {
case "info":
return "6.1.0\n", nil
case "container":
return "false\t\n", errors.New("no such container")
case "run":
return "deadbeef\n", nil
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`"}
]}`)
// It will fail at read-back — the stub never reports it running — and what matters is
// WHICH binary it used getting there.
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
for _, c := range calledWith {
if c != "podman" {
t.Errorf("called %q on a machine that only has podman", c)
}
}
if len(calledWith) == 0 {
t.Error("nothing was called; the runtime was not found")
}
}
func TestNoRuntimeIsSaidPlainly(t *testing.T) {
// Naming what was tried, because "docker: command not found" on a machine that deliberately
// runs podman sends the reader looking for the wrong thing.
run := func(ctx context.Context, name string, args ...string) (string, error) {
return "", errors.New("not installed")
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"store","type":"container","name":"store","image":"`+pinned+`"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err == nil {
t.Fatal("a machine with no container runtime applied a container")
}
for _, want := range []string{"docker", "podman", "no container runtime"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the failure does not mention %q: %v", want, err)
}
}
}
// archHost is the system these tests run against. They were written for pacman and systemd, and
// naming that is better than the implicit default it used to be.
func archHost(t *testing.T) system.System {
t.Helper()
s, err := system.For("arch")
if err != nil {
t.Fatal(err)
}
return s
}
func TestAServiceIsRestartedWhenWhatItReflectsChanges(t *testing.T) {
// A running service does not re-read its configuration. Replace the file, find the service
// already running, do nothing — and the machine keeps behaving as it did while every check
// passes, because the file is right and the service is up.
//
// That is how a third node joining a mesh left the first two carrying a network that no
// longer existed. Found in the lab; this is the shape of the fix.
dir := t.TempDir()
path := filepath.Join(dir, "thing.conf")
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"first\n","mode":"0644"},
{"id":"svc","type":"service","unit":"thing.service","state":"running","restart-on":["conf"]}
]}`, path))
var commands []string
run := recordingServices(&commands)
if _, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, run, nil, nil); err != nil {
t.Fatal(err)
} else {
// Second apply with the same content: nothing moved, so nothing restarts. A machine that
// restarted its services on every reconcile would never be steady.
commands = nil
if _, _, err := Apply(context.Background(), archHost(t), d, state,
store.OriginCarried, run, nil, nil); err != nil {
t.Fatal(err)
}
for _, c := range commands {
if strings.Contains(c, "stop") {
t.Errorf("an unchanged declaration restarted the service: %s", c)
}
}
// Now the file changes. The service is already running and must still be restarted.
changedDecl := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"second\n","mode":"0644"},
{"id":"svc","type":"service","unit":"thing.service","state":"running","restart-on":["conf"]}
]}`, path))
commands = nil
if _, _, err := Apply(context.Background(), archHost(t), changedDecl, state,
store.OriginCarried, run, nil, nil); err != nil {
t.Fatal(err)
}
var stopped, started bool
for _, c := range commands {
if strings.Contains(c, "stop thing.service") {
stopped = true
}
if strings.Contains(c, "start thing.service") {
started = true
}
}
if !stopped || !started {
t.Errorf("the file changed and the service was not restarted; commands were %v", commands)
}
reloaded, stop := -1, -1
for i, c := range commands {
if strings.Contains(c, "daemon-reload") && reloaded < 0 {
reloaded = i
}
if strings.Contains(c, "stop thing.service") && stop < 0 {
stop = i
}
}
if reloaded < 0 || reloaded > stop {
t.Errorf("the service was restarted without the unit files being read again first; commands were %v", commands)
}
}
}
func TestAServiceIsNotRestartedByAChangeItDoesNotName(t *testing.T) {
// The list is what it reflects, not everything in the declaration. A service restarted by any
// change anywhere would make every apply a fleet-wide bounce.
dir := t.TempDir()
conf := filepath.Join(dir, "thing.conf")
other := filepath.Join(dir, "unrelated")
first := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"same\n","mode":"0644"},
{"id":"other","type":"file","path":%q,"content":"one\n","mode":"0644"},
{"id":"svc","type":"service","unit":"thing.service","state":"running","restart-on":["conf"]}
]}`, conf, other))
var commands []string
run := recordingServices(&commands)
_, state, err := Apply(context.Background(), archHost(t), first, store.State{},
store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatal(err)
}
second := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"same\n","mode":"0644"},
{"id":"other","type":"file","path":%q,"content":"two\n","mode":"0644"},
{"id":"svc","type":"service","unit":"thing.service","state":"running","restart-on":["conf"]}
]}`, conf, other))
commands = nil
if _, _, err := Apply(context.Background(), archHost(t), second, state,
store.OriginCarried, run, nil, nil); err != nil {
t.Fatal(err)
}
for _, c := range commands {
if strings.Contains(c, "stop") {
t.Errorf("a change to a file the service does not name restarted it: %s", c)
}
}
}
// recordingServices answers the way a machine with a running unit would, and remembers what it
// was asked to do — which is what a restart has to be proved by, since "running" looks the same
// before and after one.
func recordingServices(commands *[]string) Runner {
return func(_ context.Context, name string, args ...string) (string, error) {
line := name + " " + strings.Join(args, " ")
*commands = append(*commands, line)
switch {
case strings.Contains(line, "is-enabled"):
return "enabled", nil
case strings.Contains(line, "show") && strings.Contains(line, "ActiveState"):
return "LoadState=loaded\nActiveState=active\nSubState=running", nil
}
return "", nil
}
}
func TestAFileChangedOnTheMachineIsCorrectedAndSaidSo(t *testing.T) {
// The question this answers: how would anybody know somebody edited a managed file? Before
// this they would not. It was rewritten within five minutes and reported as "updated",
// which is what the mesh changing its mind looks like — so the person's change vanished and
// nothing anywhere said why. They edit it again, and again.
dir := t.TempDir()
path := filepath.Join(dir, "thing.conf")
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"from the mesh\n","mode":"0644"}
]}`, path))
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
// Somebody edits it.
if err := os.WriteFile(path, []byte("edited by hand\n"), 0o644); err != nil {
t.Fatal(err)
}
report, state, err := Apply(context.Background(), archHost(t), d, state,
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if got := report.Outcomes[0].Action; got != "corrected" {
t.Errorf("a hand edit was reported as %q; the mesh cannot tell it from changing its own "+
"mind, and neither can anybody reading this", got)
}
// And it is put back, because holding the machine to what it was told is the point.
back, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(back) != "from the mesh\n" {
t.Errorf("the file was left as %q", back)
}
}
func TestTheMeshChangingItsMindIsNotDrift(t *testing.T) {
// The other half. A new declaration is an ordinary update and must not read as somebody
// having meddled, or every real change would look like an incident.
dir := t.TempDir()
path := filepath.Join(dir, "thing.conf")
first := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"one\n","mode":"0644"}
]}`, path))
second := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"two\n","mode":"0644"}
]}`, path))
_, state, err := Apply(context.Background(), archHost(t), first, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
report, _, err := Apply(context.Background(), archHost(t), second, state,
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if got := report.Outcomes[0].Action; got != "updated" {
t.Errorf("the mesh changing what it wants was reported as %q", got)
}
}
func TestAnUntouchedFileIsStillUnchanged(t *testing.T) {
// And nothing about this makes a steady machine look busy.
dir := t.TempDir()
path := filepath.Join(dir, "thing.conf")
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"conf","type":"file","path":%q,"content":"steady\n","mode":"0644"}
]}`, path))
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
report, _, err := Apply(context.Background(), archHost(t), d, state,
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if got := report.Outcomes[0].Action; got != "unchanged" {
t.Errorf("an untouched file was reported as %q", got)
}
}
func TestAFailedActionStopsWhatFollows(t *testing.T) {
// An action is the only shape whose purpose is to make something true BEFORE the next thing
// needs it, which is why it is the only one with a verify. The bootstrap is a row of them:
// the store answers, then its databases exist, then their schemas, then the broker.
//
// Carrying on past one that did not happen starts things against a machine that is not ready
// — and on a small machine that is how a database still initialising has its memory taken
// away and shuts down. Observed in the lab, caused by a version of this loop that continued
// past everything.
dir := t.TempDir()
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"gate","type":"action","command":["false"],"verify":["false"]},
{"id":"after","type":"file","path":"`+filepath.Join(dir, "after.conf")+`","content":"b\n"}
]}`)
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried,
ExecRunner, nil, nil)
if err == nil {
t.Fatal("an action that cannot succeed did not fail the apply")
}
var applyErr *Error
if !errors.As(err, &applyErr) {
t.Fatalf("got %T", err)
}
if !applyErr.Gated {
t.Error("the failure does not say that nothing after it was attempted")
}
if _, statErr := os.Stat(filepath.Join(dir, "after.conf")); statErr == nil {
t.Error("the apply continued past a failed action, which is a gate")
}
if !strings.Contains(err.Error(), "nothing after it was attempted") {
t.Errorf("the message does not say the rest was not tried: %v", err)
}
}
// A container is told which resolver to use, because it does not inherit the machine's names.
//
// A container gets its own `/etc/hosts` holding its own hostname, and a runtime rewrites
// `resolv.conf` — so every internal name the mesh wrote for the machine is invisible to what the
// machine is running. That was hit for real: a database client on one node could not resolve
// another node, on a mesh where both names were correct and present on both machines.
func TestAContainerIsGivenTheMeshsNames(t *testing.T) {
var ran []string
run := func(_ context.Context, name string, args ...string) (string, error) {
if name != "docker" {
return "", errors.New("not installed")
}
switch args[0] {
case "info":
return "29.0.0\n", nil
case "container":
return "false\t\n", errors.New("no such container")
case "run":
ran = args
return "deadbeef\n", nil
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"app","type":"container","name":"app","image":"`+pinned+`",
"hosts":["anchor.internal:10.42.0.1"]}
]}`)
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
var told bool
for i, a := range ran {
if a == "--add-host" && i+1 < len(ran) && ran[i+1] == "anchor.internal:10.42.0.1" {
told = true
}
}
if !told {
t.Fatalf("the container cannot reach another machine by name: %v", ran)
}
}
// And a container given no names is run exactly as before.
func TestAContainerGivenNoNamesIsRunAsBefore(t *testing.T) {
var ran []string
run := func(_ context.Context, name string, args ...string) (string, error) {
if name != "docker" {
return "", errors.New("not installed")
}
switch args[0] {
case "info":
return "29.0.0\n", nil
case "container":
return "false\t\n", errors.New("no such container")
case "run":
ran = args
return "deadbeef\n", nil
}
return "", nil
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"app","type":"container","name":"app","image":"`+pinned+`"}
]}`)
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
for _, a := range ran {
if a == "--add-host" {
t.Fatalf("a container given no names was given some anyway: %v", ran)
}
}
}
// Defends the ordering half of novox/hq work breakdown 1.3: resources are applied in the order
// the module declared them.
//
// **Already true, and untested until now** — the apply loop walks `d.Resources` and sorts nothing,
// so a module that needs one thing before another says so by writing it first. Worth an assertion
// because it is the kind of property a later change would break silently: sorting the resources
// for any good reason at all — by type, by identity, for a tidier report — would still pass every
// other test in this package.
//
// It is sequence, not readiness. A container started is not a container ready, and nothing here
// waits: what depends on something being *usable* retries, which is what both example
// provisioners do and is more robust than start ordering, because a dependency can also restart
// long after everything was applied.
func TestResourcesAreAppliedInTheOrderTheyWereDeclared(t *testing.T) {
dir := t.TempDir()
var order []string
done := map[string]bool{}
run := func(_ context.Context, name string, args ...string) (string, error) {
order = append(order, name+" "+strings.Join(args, " "))
// `verify` is the idempotency check, so it has to fail before the step and pass after —
// a stub that always succeeds means every action is already done and nothing ever runs,
// which is what the first version of this test measured.
if name == "check" {
if !done[args[0]] {
return "", fmt.Errorf("not yet")
}
return "", nil
}
done[args[0]] = true
return "", nil
}
_ = dir
// Trusted, because the link may not carry an action and only a bundle may (novox/hq ADR 0005).
// Actions are used here because they are the one shape whose execution is observable through
// the runner, which is what makes the order visible at all.
d, err := declaration.ParseTrusted([]byte(`{"declaration":1,"resources":[` +
`{"id":"first","type":"action","command":["step","one"],"verify":["check","one"]},` +
`{"id":"second","type":"action","command":["step","two"],"verify":["check","two"]},` +
`{"id":"third","type":"action","command":["step","three"],"verify":["check","three"]}]}`))
if err != nil {
t.Fatal(err)
}
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, run, nil, nil); err != nil {
t.Fatal(err)
}
var ran []string
for _, line := range order {
if strings.HasPrefix(line, "step ") {
ran = append(ran, strings.TrimPrefix(line, "step "))
}
}
want := []string{"one", "two", "three"}
if strings.Join(ran, ",") != strings.Join(want, ",") {
t.Fatalf("declared one, two, three and ran %v — a module that needs one thing before "+
"another has no way to say so", ran)
}
}
// **A data directory is never removed by the mesh.** The one failure in this system that cannot
// be undone.
//
// Unassigning a module made its directory an orphan, and an orphan directory was deleted with
// everything under it — a database's files, a mail spool, somebody's uploads — and the report
// said "removed". Reproduced before it was fixed: a module was assigned, a service wrote into
// its directory, the module was unassigned, and the file was gone.
//
// What makes the rule safe rather than merely cautious is the removal order. Everything the mesh
// puts in a directory is itself a declared resource, and orphans are removed in reverse
// declaration order — so what the mesh wrote is already gone by the time the directory is
// reached. Anything still there was put there by something else.
func TestADirectoryHoldingAnythingTheMeshDidNotPutThereIsKept(t *testing.T) {
root := t.TempDir()
data := filepath.Join(root, "keycloak")
d := declare(t, `{"id":"data","type":"directory","path":"`+data+`","mode":"0700"}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginDeclared, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
live := filepath.Join(data, "realm.db")
if err := os.WriteFile(live, []byte("everybody's logins"), 0o600); err != nil {
t.Fatal(err)
}
// The module is unassigned: the mesh declares something else entirely.
after := declare(t, `{"id":"other","type":"directory","path":"`+filepath.Join(root, "other")+`","mode":"0700"}`)
report, _, err := Apply(context.Background(), archHost(t), after, state,
store.OriginDeclared, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(live); err != nil {
t.Fatalf("the data was deleted by unassigning a module: %v", err)
}
// And it is said, rather than left for somebody to notice. A directory quietly left behind is
// how a machine accumulates things nobody can account for.
var said bool
for _, o := range report.Outcomes {
if o.Action == "kept" && strings.Contains(o.Detail, "did not put there") {
said = true
}
}
if !said {
t.Errorf("the directory was kept and nothing reported it: %+v", report.Outcomes)
}
}
// An empty one is the mesh's own, and goes.
func TestAnEmptyDirectoryIsStillRemoved(t *testing.T) {
root := t.TempDir()
mine := filepath.Join(root, "config")
d := declare(t, `{"id":"c","type":"directory","path":"`+mine+`","mode":"0700"}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginDeclared, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
after := declare(t, `{"id":"other","type":"directory","path":"`+filepath.Join(root, "other")+`","mode":"0700"}`)
if _, _, err := Apply(context.Background(), archHost(t), after, state,
store.OriginDeclared, nil, nil, nil); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(mine); !errors.Is(err, os.ErrNotExist) {
t.Fatal("an empty directory the mesh made was left behind, so nothing is ever cleaned up")
}
}
// A container holding values from before is replaced, even when nothing changed this pass.
//
// **This is the fault the restart-on tripwire could not catch** (novox/hq 04-ISSUES/045). That
// mechanism fires while a resource is being changed, so it covers the apply where the file moved
// and nothing afterwards. A file written in an earlier apply — or written before the container
// declared it as a dependency — leaves a process holding a credential the mesh has already
// replaced, and every check passes: the container is up, the spec matched, the machine reported
// success. Making what a container reads part of what it is turns that from a tripwire into a
// standing comparison.
func TestAContainerStaleFromAnEarlierApplyIsReplaced(t *testing.T) {
dir := t.TempDir()
conf := filepath.Join(dir, "db.env")
// Already on disk with the current content, so this apply changes nothing at all.
if err := os.WriteFile(conf, []byte("PASSWORD=new\n"), 0o600); err != nil {
t.Fatal(err)
}
d := parseTrusted(t, `{"declaration":1,"resources":[
{"id":"env","type":"file","path":"`+conf+`","content":"PASSWORD=new\n","mode":"0600"},
{"id":"app","type":"container","name":"app","image":"`+pinned+`","restart-on":["env"]}
]}`)
// The container was created when the file said something else.
was := map[string]string{"env": declaredDigest(&declaration.File{Content: "PASSWORD=old\n"})}
now := map[string]string{"env": declaredDigest(d.Resources[0].(*declaration.File))}
stale := containerSpec(d.Resources[1].(*declaration.Container), inputs{declares: was})
fresh := containerSpec(d.Resources[1].(*declaration.Container), inputs{declares: now})
var removed, created bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "info":
return "27.0\n", nil
case "container":
if created {
return "true\t" + fresh, nil
}
return "true\t" + stale, nil
case "rm":
removed = true
return "", nil
case "run":
created = true
return "deadbeef\n", nil
}
return "", nil
}
_, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
if err != nil {
t.Fatalf("apply failed: %v", err)
}
if !removed || !created {
t.Fatalf("a container running values the machine no longer holds was left alone "+
"(removed=%v created=%v)", removed, created)
}
}
// A seed is written once. What grows in it afterwards is somebody else's work the mesh asked for,
// and a reconcile leaves it alone — content, mode and owner — and says so (novox/hq issue 035).
func TestASeedIsCreatedOnceAndWhatGrowsInItIsKept(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "acl.conf")
d := parse(t, fmt.Sprintf(`{"declaration":1,"resources":[
{"id":"acl","type":"file","path":%q,"content":"user default on\n","mode":"0600","create-once":true}
]}`, path))
report, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if got := report.Outcomes[0].Action; got != "created" {
t.Fatalf("first apply: %q", got)
}
// The program persists into it.
if err := os.WriteFile(path, []byte("user default on\nuser app-one on >secret\n"), 0o600); err != nil {
t.Fatal(err)
}
report, _, err = Apply(context.Background(), archHost(t), d, state,
store.OriginCarried, noServices, nil, nil)
if err != nil {
t.Fatal(err)
}
if got := report.Outcomes[0].Action; got != "kept" {
t.Fatalf("second apply: %q — a seed was reconciled", got)
}
grown, _ := os.ReadFile(path)
if string(grown) != "user default on\nuser app-one on >secret\n" {
t.Fatalf("what grew in the seed was wiped: %q", grown)
}
}