mesh/merge-gate head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: [release feat/57-config-and-provisioning, judg…
mesh/delivery stopped: its walk plan-1792114347281764422 was stopped by a person (mesh-controller): mesh-host#114 merged into the release branch feat/57-…
mesh/delivery-group group 57-config-and-provisioning stopped: a member was stopped
The node-engine renders the feature's templates, collections and moved providers' files, on the SDK the feature's modules build on (feature 57, config and provisioning).
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")))
|
|
}
|