mesh/merge-gate head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group release/57-config-and-provisioning delivered: every member is delivered
219 lines
8.8 KiB
Go
219 lines
8.8 KiB
Go
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")))
|
|
}
|