package apply import ( "context" "fmt" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq ADR 0349 §5, one writer per file (design 57 P7, issue 673): a file the declaration names as // filled is written only through the write verb (WriteFilled), on the provisioner client's word. An apply — the // controller's send and the five-minute re-apply alike — never writes it, never records it, and removes it as an // orphan only when the verb placed it. // An apply leaves a filled file alone, whatever who owns it at runtime says: absent stays absent and is not // recorded, and what the verb wrote stays as it wrote it through every re-apply. func TestAnApplyNeverWritesAFilledFile(t *testing.T) { for _, c := range []struct{ name, fields, content string }{ {"owned", `"content":"","mode":"0640","owner-at-runtime":"owned"`, "port = 8080\n"}, {"seed", `"content":"","mode":"0600","owner-at-runtime":"seed"`, "user default on\n"}, {"managed block", `"into":"block","content":"","owner-at-runtime":"managed"`, "10.42.0.1 ace\n"}, } { t.Run(c.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), "the-file") d := filledDecl(t, path, c.fields, "") report, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if _, err := os.Stat(path); err == nil { t.Fatalf("an apply wrote a filled file the client has not filled: %q", readText(t, path)) } if _, ok := state.Find("m.conf"); ok { t.Errorf("an apply recorded a filled file it did not write") } if o := outcomeOf(report, "m.conf"); o.Action != "unchanged" || !strings.Contains(o.Detail, "provisioner") { t.Errorf("an apply did not say it left the file to the provisioner's client: %+v", o) } _, state, err = WriteFilled(context.Background(), archHost(t), d, state, path, []byte(c.content), noServices, nil, nil, nil) if err != nil { t.Fatal(err) } placed, _ := state.Find("m.conf") want, wantMode := readText(t, path), modeAt(t, path) // The re-apply, twice over: nothing written, the verb's record kept as it wrote it. for i := 0; i < 2; i++ { var said []string report, state, err = Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, noServices, func(line string) { said = append(said, line) }, nil) if err != nil { t.Fatal(err) } if got := readText(t, path); got != want || modeAt(t, path) != wantMode { t.Fatalf("re-apply %d wrote the filled file again: %q, want %q", i+1, got, want) } if o := outcomeOf(report, "m.conf"); o.Action != "unchanged" { t.Errorf("re-apply %d: %+v", i+1, o) } if rec, _ := state.Find("m.conf"); rec.Wrote != placed.Wrote || rec.Target != path { t.Errorf("re-apply %d changed the verb's record: %+v, was %+v", i+1, rec, placed) } if len(said) != 0 { t.Errorf("re-apply %d said something of an untouched file: %q", i+1, said) } } }) } } // What reads a filled file is moved by the verb's write, once, never again by an apply: a service under // restart-on is not restarted and a container reading it is not recreated by the re-apply. func TestAnApplyMovesNothingForAFilledFile(t *testing.T) { env := filepath.Join(t.TempDir(), "forge.env") d := filledDecl(t, env, `"content":"","mode":"0600","owner-at-runtime":"owned"`, `{"id":"forge.server","type":"container","name":"forge","image":"`+pinned+`","env-file":["`+env+`"]}`) m := &machine{containers: map[string]*fakeContainer{}} _, state, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, env, []byte("PORT=5432\n"), m.run, nil, nil, nil) if err != nil { t.Fatal(err) } m.asked = nil if _, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, m.run, nil, nil); err != nil { t.Fatal(err) } if m.removed("forge") || m.did("docker run") { t.Errorf("the re-apply recreated the container reading the filled file: %v", m.asked) } if got := readText(t, env); got != "PORT=5432\n" { t.Errorf("the re-apply wrote the filled file: %q", got) } conf := filepath.Join(t.TempDir(), "daemon.conf") d = filledDecl(t, conf, `"content":"","owner-at-runtime":"owned"`, `{"id":"m.daemon","type":"service","unit":"m.service","state":"running","restart-on":["m.conf"]}`) var commands []string _, state, err = WriteFilled(context.Background(), archHost(t), d, store.State{}, conf, []byte("level = 1\n"), recordingServices(&commands), nil, nil, nil) if err != nil { t.Fatal(err) } commands = nil if _, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, recordingServices(&commands), nil, nil); err != nil { t.Fatal(err) } if n := count(commands, "systemctl stop m.service"); n != 0 { t.Errorf("the re-apply restarted what reads the filled file: %v", commands) } } // A filled file the verb never placed is not the mesh's to remove: one found there, or put there by hand, stays // when the file leaves the declaration. func TestAFilledFileTheVerbNeverPlacedIsNotRemoved(t *testing.T) { path := filepath.Join(t.TempDir(), "the-file") if err := os.WriteFile(path, []byte("the machine's own\n"), 0o644); err != nil { t.Fatal(err) } d := filledDecl(t, path, `"content":"","owner-at-runtime":"owned"`, "") _, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } gone := elsewhere(t) report, state, err := Apply(context.Background(), archHost(t), gone, state, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if got := readText(t, path); got != "the machine's own\n" { t.Errorf("a filled file the verb never placed was touched when it left: %q", got) } if o := outcomeOf(report, "m.conf"); o.Action == "removed" { t.Errorf("a filled file the verb never placed was removed: %+v", o) } if _, ok := state.Find("m.conf"); ok { t.Errorf("a record of the filled file was left") } } // What the verb placed is the mesh's: when the file leaves the declaration it is removed as an orphan, as a file // an apply wrote is. func TestAFilledFileTheVerbPlacedIsRemovedWhenItLeaves(t *testing.T) { path := filepath.Join(t.TempDir(), "the-file") d := filledDecl(t, path, `"content":"","owner-at-runtime":"owned"`, "") _, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } _, state, err = WriteFilled(context.Background(), archHost(t), d, state, path, []byte("port = 8080\n"), noServices, nil, nil, nil) if err != nil { t.Fatal(err) } gone := elsewhere(t) report, state, err := Apply(context.Background(), archHost(t), gone, state, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if _, err := os.Stat(path); err == nil { t.Errorf("a filled file the verb placed stayed when it left the declaration: %q", readText(t, path)) } if o := outcomeOf(report, "m.conf"); o.Action != "removed" { t.Errorf("not removed: %+v", o) } if _, ok := state.Find("m.conf"); ok { t.Errorf("its record stayed") } } // A file the mesh wrote the old way and that is now filled keeps its record for the verb to judge against, and is // not written over by the apply that hands it to the client; a file at the same path recorded under another id // is handed to the filled one, not removed. func TestAFileThatMovesToTheClientIsHandedNotRewritten(t *testing.T) { path := filepath.Join(t.TempDir(), "the-file") old := parseTrusted(t, fmt.Sprintf(`{"declaration":1,"resources":[{"id":"m.old","type":"file","path":%q,`+ `"content":"port = 1\n"}]}`, path)) _, state, err := Apply(context.Background(), archHost(t), old, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } was, _ := state.Find("m.old") d := filledDecl(t, path, `"content":"","owner-at-runtime":"owned"`, "") _, state, err = Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if got := readText(t, path); got != "port = 1\n" { t.Errorf("the file handed to the client was rewritten or removed: %q", got) } if _, ok := state.Find("m.old"); ok { t.Errorf("the record going was kept beside the filled file's") } if rec, ok := state.Find("m.conf"); !ok || rec.Wrote != was.Wrote || rec.Target != path { t.Errorf("the filled file did not take the record of what the mesh wrote there: %+v", rec) } } // elsewhere is a declaration with nothing at the filled path: one unrelated file. func elsewhere(t *testing.T) *declaration.Declaration { t.Helper() return parseTrusted(t, fmt.Sprintf(`{"declaration":1,"resources":[{"id":"n.other","type":"file","path":%q,`+ `"content":"x\n"}]}`, filepath.Join(t.TempDir(), "other"))) }