Stage 2 begins. The host stops only reporting and starts doing its one job (ADR 0037): take an ordered list of typed resources and make the machine match it, from a local file, with no mesh present. What lands in this slice — the network-free vocabulary ADR 0043 names first: - Parse: JSON, refused WHOLE on an unknown version, type, field, a missing id/type/path, or a duplicate id. An older host cannot be handed a newer vocabulary and do half of it. - directory and file appliers, each reading back after it writes — mode and owner asserted against the machine, content compared byte for byte. A value that did not take is a failed apply, not a success. - store: the applied-state record, authoritative while disconnected, written atomically. It is what makes removal possible. - Convergence: apply in the stated order (the host never reorders), record each success AFTER it works (ADR 0035), and remove what was applied before and is no longer declared — in reverse order, so a file goes before the directory that held it. - The data-loss guard: the host removes ONLY what it created, never what it adopted, and a created directory that now holds data is refused (os.Remove, never RemoveAll) rather than deleted (ADR 0018, 0030). created is sticky across re-applies — caught by running the real binary, not just the unit tests: recomputing it from disk made a re-applied resource look adopted and leak on the next drop. - Addressing: a declaration for another node is refused; a host with no identity yet applies its bundle (the first-node path). Not yet: sealed secrets, and the types that need the network or a runtime (container, package, network, service, archive, user, action) — they follow, and until then the host refuses them rather than doing part of a declaration. CLI: mesh-host apply [--store P] FILE. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
314 lines
12 KiB
Go
314 lines
12 KiB
Go
package apply
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
// A declaration this host does not fully understand is refused whole — the property the whole
|
|
// project is built around, tested at the boundary it matters most.
|
|
func TestParseRefusesWhatItCannotFullyUnderstand(t *testing.T) {
|
|
cases := map[string]string{
|
|
"unknown version": `{"version":2,"resources":[]}`,
|
|
"unknown type": `{"version":1,"resources":[
|
|
{"id":"a","type":"container","path":"/x"}]}`,
|
|
"unknown field on a known type": `{"version":1,"resources":[
|
|
{"id":"a","type":"directory","path":"/x","colour":"blue"}]}`,
|
|
"unknown top-level field": `{"version":1,"nodes":[],"resources":[]}`,
|
|
"resource without an id": `{"version":1,"resources":[{"type":"directory","path":"/x"}]}`,
|
|
"resource without a type": `{"version":1,"resources":[{"id":"a","path":"/x"}]}`,
|
|
"resource without a path": `{"version":1,"resources":[{"id":"a","type":"directory"}]}`,
|
|
"two resources sharing id": `{"version":1,"resources":[{"id":"a","type":"directory","path":"/x"},{"id":"a","type":"directory","path":"/y"}]}`,
|
|
}
|
|
for name, raw := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := Parse([]byte(raw)); err == nil {
|
|
t.Fatalf("accepted a declaration it should have refused whole")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAcceptsAWellFormedDeclaration(t *testing.T) {
|
|
raw := `{"version":1,"for":"anchor","resources":[
|
|
{"id":"state","type":"directory","path":"/var/lib/x","mode":"0700"},
|
|
{"id":"conf","type":"file","path":"/var/lib/x/conf","mode":"0600","content":"k=v\n"}]}`
|
|
d, err := Parse([]byte(raw))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d.For != "anchor" || len(d.Resources) != 2 {
|
|
t.Fatalf("parsed wrong: %+v", d)
|
|
}
|
|
if d.Resources[0].Path() != "/var/lib/x" {
|
|
t.Fatalf("path accessor wrong: %q", d.Resources[0].Path())
|
|
}
|
|
}
|
|
|
|
// Applying a declaration lands the resources, and applying it again changes nothing — the
|
|
// idempotency the node host promises.
|
|
func TestApplyIsIdempotent(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "store.json")
|
|
dir := filepath.Join(root, "svc")
|
|
file := filepath.Join(dir, "conf")
|
|
|
|
decl := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`","mode":"0755"},
|
|
{"id":"f","type":"file","path":"`+file+`","mode":"0644","content":"hello\n"}]}`)
|
|
|
|
for i := 0; i < 2; i++ {
|
|
store, err := LoadStore(storePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := Apply(decl, "", store, Appliers())
|
|
if err != nil {
|
|
t.Fatalf("apply %d: %v", i, err)
|
|
}
|
|
if len(res.Applied) != 2 {
|
|
t.Fatalf("apply %d: expected 2 applied, got %d", i, len(res.Applied))
|
|
}
|
|
}
|
|
if got, _ := os.ReadFile(file); string(got) != "hello\n" {
|
|
t.Fatalf("file content wrong: %q", got)
|
|
}
|
|
}
|
|
|
|
// A resource dropped from a declaration is removed on the next apply — but only because the host
|
|
// created it. This is desired-state convergence with the data-loss guard intact.
|
|
func TestUndeclaredResourceIsRemovedWhenHostCreatedIt(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "store.json")
|
|
dir := filepath.Join(root, "svc")
|
|
gone := filepath.Join(dir, "gone")
|
|
kept := filepath.Join(dir, "kept")
|
|
|
|
first := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`"},
|
|
{"id":"gone","type":"file","path":"`+gone+`","content":"x"},
|
|
{"id":"kept","type":"file","path":"`+kept+`","content":"y"}]}`)
|
|
store, _ := LoadStore(storePath)
|
|
if _, err := Apply(first, "", store, Appliers()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(gone); err != nil {
|
|
t.Fatalf("first apply did not create the file: %v", err)
|
|
}
|
|
|
|
second := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`"},
|
|
{"id":"kept","type":"file","path":"`+kept+`","content":"y"}]}`)
|
|
store, _ = LoadStore(storePath)
|
|
res, err := Apply(second, "", store, Appliers())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(res.Removed) != 1 || res.Removed[0].ID != "gone" {
|
|
t.Fatalf("expected 'gone' removed, got %+v", res.Removed)
|
|
}
|
|
if _, err := os.Stat(gone); !os.IsNotExist(err) {
|
|
t.Fatalf("the undeclared file was not removed")
|
|
}
|
|
if _, err := os.Stat(kept); err != nil {
|
|
t.Fatalf("the still-declared file was wrongly removed: %v", err)
|
|
}
|
|
}
|
|
|
|
// "created" is sticky across re-applies. A resource created once, then re-applied (so it already
|
|
// exists the second time), must still be removed when later dropped — the host does not forget it
|
|
// was the creator just because the resource was present on a subsequent apply.
|
|
func TestCreatedIsStickyAcrossReapplies(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "store.json")
|
|
dir := filepath.Join(root, "svc")
|
|
file := filepath.Join(dir, "conf")
|
|
|
|
full := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`"},
|
|
{"id":"f","type":"file","path":"`+file+`","content":"x"}]}`)
|
|
// Apply it twice. On the second apply everything already exists, so a naive "created" would
|
|
// flip to false and the file would later be treated as adopted.
|
|
for i := 0; i < 2; i++ {
|
|
store, _ := LoadStore(storePath)
|
|
if _, err := Apply(full, "", store, Appliers()); err != nil {
|
|
t.Fatalf("apply %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
dropped := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`"}]}`)
|
|
store, _ := LoadStore(storePath)
|
|
res, err := Apply(dropped, "", store, Appliers())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(res.Removed) != 1 || res.Removed[0].ID != "f" {
|
|
t.Fatalf("re-applied-then-dropped file was not removed: %+v", res.Removed)
|
|
}
|
|
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
|
t.Fatal("host reported the file removed but it is still on disk (created flag was not sticky)")
|
|
}
|
|
}
|
|
|
|
// The host never removes what it did not create. A directory it merely adopted — one that
|
|
// already existed, holding data — survives being dropped from the declaration.
|
|
func TestAdoptedResourceIsNeverRemoved(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "store.json")
|
|
existing := filepath.Join(root, "data") // pre-exists: the host will adopt, not create it
|
|
if err := os.Mkdir(existing, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sentinel := filepath.Join(existing, "precious")
|
|
if err := os.WriteFile(sentinel, []byte("workload data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
first := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"data","type":"directory","path":"`+existing+`","mode":"0755"}]}`)
|
|
store, _ := LoadStore(storePath)
|
|
res, err := Apply(first, "", store, Appliers())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Applied[0].Created {
|
|
t.Fatalf("host claimed to have created a directory that already existed")
|
|
}
|
|
|
|
// Drop it from the declaration entirely. An adopted directory is not the host's to remove.
|
|
empty := mustParse(t, `{"version":1,"resources":[]}`)
|
|
store, _ = LoadStore(storePath)
|
|
if _, err := Apply(empty, "", store, Appliers()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(sentinel); err != nil {
|
|
t.Fatalf("adopted directory (and its data) was removed — the guard failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// A failed step fails the apply, and the store records what actually landed — never more.
|
|
func TestFailedStepFailsTheApplyAndRecordsOnlyWhatLanded(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "store.json")
|
|
good := filepath.Join(root, "good")
|
|
// The second file's parent does not exist, so writing it fails — a mid-declaration failure.
|
|
bad := filepath.Join(root, "nonexistent-dir", "bad")
|
|
|
|
decl := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"good","type":"file","path":"`+good+`","content":"ok"},
|
|
{"id":"bad","type":"file","path":"`+bad+`","content":"no"}]}`)
|
|
store, _ := LoadStore(storePath)
|
|
if _, err := Apply(decl, "", store, Appliers()); err == nil {
|
|
t.Fatal("apply reported success despite a step that could not be done")
|
|
}
|
|
|
|
// The store must record 'good' (it landed) and not 'bad' (it did not).
|
|
reloaded, _ := LoadStore(storePath)
|
|
ids := map[string]bool{}
|
|
for _, r := range reloaded.Records() {
|
|
ids[r.ID] = true
|
|
}
|
|
if !ids["good"] {
|
|
t.Fatalf("the store forgot a resource that actually landed")
|
|
}
|
|
if ids["bad"] {
|
|
t.Fatalf("the store recorded a resource that never landed — a report from intent")
|
|
}
|
|
}
|
|
|
|
// A declaration addressed to another node is refused; one addressed here, or unaddressed, is
|
|
// applied.
|
|
func TestAddressing(t *testing.T) {
|
|
root := t.TempDir()
|
|
dir := filepath.Join(root, "d")
|
|
decl := mustParse(t, `{"version":1,"for":"anchor","resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`"}]}`)
|
|
|
|
store, _ := LoadStore(filepath.Join(root, "s1.json"))
|
|
if _, err := Apply(decl, "workstation", store, Appliers()); err == nil {
|
|
t.Fatal("a node applied a declaration addressed to a different node")
|
|
}
|
|
|
|
store, _ = LoadStore(filepath.Join(root, "s2.json"))
|
|
if _, err := Apply(decl, "anchor", store, Appliers()); err != nil {
|
|
t.Fatalf("a node refused a declaration addressed to it: %v", err)
|
|
}
|
|
|
|
// The first node — no identity yet — applies whatever it carries.
|
|
store, _ = LoadStore(filepath.Join(root, "s3.json"))
|
|
if _, err := Apply(decl, "", store, Appliers()); err != nil {
|
|
t.Fatalf("a node with no identity refused its own bundle: %v", err)
|
|
}
|
|
}
|
|
|
|
// Read-back catches a value that did not take. Mode and owner are asserted against the machine
|
|
// after applying, so a file written with the wrong permissions is a failure, not a success.
|
|
func TestApplyReadsModeBack(t *testing.T) {
|
|
root := t.TempDir()
|
|
file := filepath.Join(root, "f")
|
|
decl := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"f","type":"file","path":"`+file+`","mode":"0600","content":"x"}]}`)
|
|
store, _ := LoadStore(filepath.Join(root, "s.json"))
|
|
if _, err := Apply(decl, "", store, Appliers()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, _ := os.Stat(file)
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("mode not applied: %04o", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
// A directory the host created but that now holds something is not removed — os.Remove refuses a
|
|
// non-empty directory, and that refusal is the guard, surfaced as a failed apply.
|
|
func TestCreatedDirectoryHoldingDataIsNotSilentlyDeleted(t *testing.T) {
|
|
root := t.TempDir()
|
|
storePath := filepath.Join(root, "store.json")
|
|
dir := filepath.Join(root, "svc")
|
|
|
|
first := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`"}]}`)
|
|
store, _ := LoadStore(storePath)
|
|
if _, err := Apply(first, "", store, Appliers()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Something drops data into the host-created directory after the fact.
|
|
if err := os.WriteFile(filepath.Join(dir, "appeared"), []byte("data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
empty := mustParse(t, `{"version":1,"resources":[]}`)
|
|
store, _ = LoadStore(storePath)
|
|
if _, err := Apply(empty, "", store, Appliers()); err == nil {
|
|
t.Fatal("host deleted, or claimed to delete, a non-empty directory it once created")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "appeared")); err != nil {
|
|
t.Fatalf("data in the directory was lost: %v", err)
|
|
}
|
|
}
|
|
|
|
func mustParse(t *testing.T, raw string) Declaration {
|
|
t.Helper()
|
|
d, err := Parse([]byte(raw))
|
|
if err != nil {
|
|
t.Fatalf("test declaration did not parse: %v", err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Sanity: the current uid is what owner read-back compares against, so an owner naming this user
|
|
// verifies rather than needing root.
|
|
func TestOwnerReadBackAgainstCurrentUser(t *testing.T) {
|
|
root := t.TempDir()
|
|
dir := filepath.Join(root, "d")
|
|
owner := strconv.Itoa(os.Getuid()) + ":" + strconv.Itoa(os.Getgid())
|
|
decl := mustParse(t, `{"version":1,"resources":[
|
|
{"id":"d","type":"directory","path":"`+dir+`","mode":"0755","owner":"`+owner+`"}]}`)
|
|
store, _ := LoadStore(filepath.Join(root, "s.json"))
|
|
if _, err := Apply(decl, "", store, Appliers()); err != nil {
|
|
t.Fatalf("applying an owner matching the current user failed: %v", err)
|
|
}
|
|
}
|