Complete the host's vocabulary: package, container, action

The three shapes the substrate bootstrap needs and the host did not have. Until
now tier 1 could not be raised at all -- step 0 is a package, step 1 a
container, steps 2 and 3 actions -- so every line of the tier 1 and 2 designs
was unbuildable.

package -- present, never upgraded, never uninstalled. Removal is "forgotten",
not "removed": 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 saw it. Reporting it
removed would claim an effect the host declined to have.

container -- identified by a label carrying a digest of the declaration that
made it. Comparing every field the runtime reports cannot be done reliably: a
runtime normalises, defaults and reorders what it is given, and that is
indistinguishable from real drift. There is no in-place update; a container's
configuration is fixed at creation, so any change is a replacement, and saying
so beats a partial update that leaves the running thing half-declared. This is
the one shape the host removes, because it is the one the host created.

action -- bundle-only, per ADR 0047. Verify is mandatory and does double duty:
it 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. `in` runs the action inside a named container, which steps 2 and 3
need.

Parse now refuses actions; ParseTrusted permits them. The safe path is the
default and the permissive one has to be named. The bundle and a local file
handed to a root process use ParseTrusted; the link will use Parse.

Also replaced the per-type "fields this type ignores" check with a field-set
diff stated as what each type USES. The negative form needs every type revisited
whenever a field is added, and the one nobody revisits silently accepts a field
it will never read.

Images must be pinned by digest (ADR 0046). A bundle naming a tag pins nothing.

Verified against a real machine, not only fakes: an action ran and was
idempotent on the second apply; an action that exits zero and satisfies nothing
fails the apply; a real container was created, labelled, replaced when its
declaration changed, exec'd into, and removed; a real package query round-
tripped. Each new test was also confirmed to fail on an injected fault -- five
injections, each breaking exactly its own test.

One existing test changed: a vanished unit is now reported "forgotten" rather
than "removed", which is what actually happened.
This commit is contained in:
2026-08-27 20:36:27 +02:00
parent 08a1263a81
commit 337126603e
6 changed files with 825 additions and 39 deletions
+276 -2
View File
@@ -407,7 +407,281 @@ func TestForgettingAUnitThatIsGoneDoesNotStrandTheNode(t *testing.T) {
if _, still := state.Find("gone"); still {
t.Error("the host still believes it owns a unit that is gone")
}
if report.Outcomes[0].Action != "removed" {
t.Errorf("the vanished unit was not reported as removed: %+v", report.Outcomes)
// "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-substrate.md, ADR 0046, ADR 0047) ---
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(), d, store.State{}, run, 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(), d, store.State{}, run, 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(), d, known, run, 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(), d, store.State{}, run, 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(), d, store.State{}, run, 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(), d, store.State{}, run, 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] == "version":
return "27.0\n", nil
case args[0] == "inspect":
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(), d, store.State{}, run, 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])
var removed, created bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "version":
return "27.0\n", nil
case "inspect":
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(), d, store.State{}, run, 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])
var touched bool
run := func(ctx context.Context, name string, args ...string) (string, error) {
switch args[0] {
case "version":
return "27.0\n", nil
case "inspect":
return "true\t" + spec, nil
}
touched = true
return "", nil
}
report, _, err := Apply(context.Background(), d, store.State{}, run, 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)
}
}