package apply import ( "context" "errors" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq ADR 0100: on an adopted node, what is found is kept until its module is taken. // machine is a fake container runtime holding containers by name: id, running, and the host's spec // label when a host made it. Every command it is asked is written down. type machine struct { containers map[string]*fakeContainer asked []string } type fakeContainer struct { id string running bool spec string } func (m *machine) run(_ context.Context, name string, args ...string) (string, error) { m.asked = append(m.asked, name+" "+strings.Join(args, " ")) switch args[0] { case "info": return "27.0\n", nil case "inspect": c, ok := m.containers[args[len(args)-1]] if !ok { return "", errors.New("no such container") } running := "false" if c.running { running = "true" } if strings.HasPrefix(args[2], "{{.Id}}") { return c.id + "\t" + running + "\t" + c.spec + "\n", nil } return running + "\t" + c.spec + "\n", nil case "rm": delete(m.containers, args[len(args)-1]) return "", nil case "run": var name, spec string for i, a := range args { if a == "--name" { name = args[i+1] } if a == "--label" && strings.HasPrefix(args[i+1], specLabel+"=") { spec = strings.TrimPrefix(args[i+1], specLabel+"=") } } m.containers[name] = &fakeContainer{id: "made-by-host", running: true, spec: spec} return "made-by-host\n", nil } return "", nil } func (m *machine) removed(name string) bool { for _, a := range m.asked { if strings.HasPrefix(a, "docker rm") && strings.HasSuffix(a, " "+name) { return true } } return false } func adopted(t *testing.T, adoption, resources string) *declaration.Declaration { t.Helper() return parse(t, `{"declaration":1,"adoption":`+adoption+`,"resources":[`+resources+`]}`) } const untakenWeb = `{"taken":[],"untaken":{"hello-web":["hello-web.page","hello-web.server"]}}` const takenWeb = `{"taken":["hello-web"]}` func webResources(page string) string { return `{"id":"hello-web.page","type":"file","path":"` + page + `","content":"the mesh's page\n"}, {"id":"hello-web.server","type":"container","name":"hello-web","image":"` + pinned + `"}` } func applyAdopted(t *testing.T, d *declaration.Declaration, known store.State, m *machine, keepDir string) (Report, store.State) { t.Helper() report, state, err := ApplyKeeping(context.Background(), archHost(t), d, known, store.OriginDeclared, m.run, nil, nil, KeepIn(keepDir)) if err != nil { t.Fatalf("apply failed: %v", err) } return report, state } func outcomeOf(r Report, id string) Outcome { for _, o := range r.Outcomes { if o.ID == id { return o } } return Outcome{} } func predecessor(t *testing.T) (dir, page string, m *machine) { t.Helper() dir = t.TempDir() page = filepath.Join(dir, "index.html") if err := os.WriteFile(page, []byte("the predecessor's page\n"), 0o640); err != nil { t.Fatal(err) } return dir, page, &machine{containers: map[string]*fakeContainer{ "hello-web": {id: "predecessor-id", running: true}, }} } func TestAFoundFileOfAnUntakenModuleIsKeptAsItIs(t *testing.T) { dir, page, m := predecessor(t) report, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) got, _ := os.ReadFile(page) if string(got) != "the predecessor's page\n" { t.Fatalf("a found file was changed: %q", got) } info, _ := os.Stat(page) if info.Mode().Perm() != 0o640 { t.Errorf("a found file's mode was changed to %o", info.Mode().Perm()) } if o := outcomeOf(report, "hello-web.page"); o.Action != "held" || !strings.Contains(o.Detail, "kept until hello-web is taken") { t.Errorf("the found file was not reported held: %+v", o) } h, ok := state.HeldAt("hello-web.page") if !ok || h.Module != "hello-web" || h.Mode != "0640" { t.Fatalf("the hold was not recorded: %+v", h) } kept, err := os.ReadFile(h.Kept) if err != nil || string(kept) != "the predecessor's page\n" { t.Fatalf("the original was not kept: %q %v", kept, err) } if info, _ := os.Stat(h.Kept); info.Mode().Perm() != 0o600 { t.Errorf("the kept original is mode %o", info.Mode().Perm()) } if _, recorded := state.Find("hello-web.page"); recorded { t.Error("a held file was recorded as applied, so it would be removed as an orphan") } if report.Changed() { t.Errorf("holding was reported as changing the machine: %+v", report.Outcomes) } } func TestAFoundContainerOfAnUntakenModuleIsNotReplaced(t *testing.T) { dir, page, m := predecessor(t) report, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) if m.removed("hello-web") { t.Fatal("a found container was removed") } for _, a := range m.asked { if strings.HasPrefix(a, "docker run") { t.Fatalf("a container was started over a found one: %s", a) } } if outcomeOf(report, "hello-web.server").Action != "held" { t.Errorf("the found container was not held: %+v", report.Outcomes) } if h, _ := state.HeldAt("hello-web.server"); h.Container != "predecessor-id" || !h.Running { t.Errorf("the container as found was not recorded: %+v", h) } } func TestWhatIsNotFoundIsCreatedWhenAssigned(t *testing.T) { // Assigning prepares: what the module declares that is not there is made. dir := t.TempDir() page := filepath.Join(dir, "index.html") m := &machine{containers: map[string]*fakeContainer{}} report, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) if o := outcomeOf(report, "hello-web.page"); o.Action != "created" { t.Errorf("an absent file of an untaken module was not created: %+v", o) } if o := outcomeOf(report, "hello-web.server"); o.Action != "created" { t.Errorf("an absent container of an untaken module was not created: %+v", o) } if len(state.Held) != 0 { t.Errorf("something was held that was not found: %+v", state.Held) } } func TestAFileThisHostWroteIsNotFound(t *testing.T) { // Found means present with no record. A record of any origin — carried or declared, this life // of the node or an earlier one — means this host wrote it. for _, origin := range []string{store.OriginCarried, store.OriginDeclared} { dir, page, m := predecessor(t) known := store.State{Resources: []store.Applied{ {ID: "earlier-name", Type: "file", Target: page, Origin: origin}}} report, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), known, m, dir) if o := outcomeOf(report, "hello-web.page"); o.Action == "held" { t.Errorf("%s: a file this host has a record of was held: %+v", origin, o) } if _, held := state.HeldAt("hello-web.page"); held { t.Errorf("%s: a recorded file was held", origin) } } } func TestAContainerAHostMadeIsNotFound(t *testing.T) { dir, page, m := predecessor(t) m.containers["hello-web"].spec = "some-spec" report, _ := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) if o := outcomeOf(report, "hello-web.server"); o.Action == "held" { t.Errorf("a container carrying the host's spec label was held: %+v", o) } } func TestTheGenesisStoreAdoptedInPlaceIsNotFound(t *testing.T) { // ADR 0078: the foundation's store, raised from the bundle and recorded as carried, is adopted // as a module by name. It is the mesh's own and must never read as a predecessor's. dir := t.TempDir() m := &machine{containers: map[string]*fakeContainer{"mesh-store": {id: "x", running: true}}} known := store.State{Resources: []store.Applied{{ID: "store", Type: "container", Target: "mesh-store"}}} d := adopted(t, `{"taken":[],"untaken":{"postgres":["postgres.server"]}}`, `{"id":"postgres.server","type":"container","name":"mesh-store","image":"`+pinned+`"}`) report, state := applyAdopted(t, d, known, m, dir) if o := outcomeOf(report, "postgres.server"); o.Action == "held" { t.Errorf("the carried store was held: %+v", o) } if len(state.Held) != 0 { t.Errorf("the carried store was held: %+v", state.Held) } } func TestTakingAModuleReplacesWhatWasHeldAndTheOriginalSurvives(t *testing.T) { dir, page, m := predecessor(t) _, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) h, _ := state.HeldAt("hello-web.page") report, state := applyAdopted(t, adopted(t, takenWeb, webResources(page)), state, m, dir) got, _ := os.ReadFile(page) if string(got) != "the mesh's page\n" { t.Fatalf("taking the module did not converge the file: %q", got) } if !m.removed("hello-web") || m.containers["hello-web"].id != "made-by-host" { t.Fatal("taking the module did not replace the found container") } if o := outcomeOf(report, "hello-web.page"); !strings.Contains(o.Detail, "original kept at "+h.Kept) { t.Errorf("the cutover does not say where the original is: %+v", o) } if len(state.Held) != 0 { t.Errorf("what was taken is still held: %+v", state.Held) } if _, recorded := state.Find("hello-web.page"); !recorded { t.Error("a taken file was not recorded as applied") } kept, err := os.ReadFile(h.Kept) if err != nil || string(kept) != "the predecessor's page\n" { t.Errorf("the kept original did not survive the cutover: %q %v", kept, err) } } func TestAHeldFileIsNeverRemovedWhenItsModuleIsUnassigned(t *testing.T) { dir, page, m := predecessor(t) _, state := applyAdopted(t, adopted(t, untakenWeb, webResources(page)), store.State{}, m, dir) other := filepath.Join(dir, "other") _, state = applyAdopted(t, adopted(t, `{"taken":[]}`, `{"id":"x.other","type":"file","path":"`+other+`","content":"x"}`), state, m, dir) if got, _ := os.ReadFile(page); string(got) != "the predecessor's page\n" { t.Fatalf("a held file was touched when its module left: %q", got) } if m.removed("hello-web") { t.Fatal("a held container was removed when its module left") } if _, still := state.HeldAt("hello-web.page"); !still { t.Error("the hold was forgotten, so a return of the module would read the file as the mesh's") } } func TestAHeldFileRewrittenIsReportedAndNotReverted(t *testing.T) { dir, page, m := predecessor(t) d := adopted(t, untakenWeb, webResources(page)) _, state := applyAdopted(t, d, store.State{}, m, dir) if err := os.WriteFile(page, []byte("the predecessor wrote again\n"), 0o640); err != nil { t.Fatal(err) } report, state := applyAdopted(t, d, state, m, dir) if got, _ := os.ReadFile(page); string(got) != "the predecessor wrote again\n" { t.Fatalf("a held file was reverted: %q", got) } if h, _ := state.HeldAt("hello-web.page"); h.Changed != "rewritten" || h.ChangedAt.IsZero() { t.Errorf("a rewrite was not recorded: %+v", h) } if o := outcomeOf(report, "hello-web.page"); !strings.Contains(o.Detail, "rewritten") { t.Errorf("a rewrite was not reported: %+v", o) } h, _ := state.HeldAt("hello-web.page") if kept, _ := os.ReadFile(h.Kept); string(kept) != "the predecessor's page\n" { t.Errorf("the kept original was overwritten by a later write: %q", kept) } } func TestAHeldContainerStoppedOrReplacedIsReportedAndNotRestarted(t *testing.T) { for _, c := range []struct { change func(*machine) want string }{ {func(m *machine) { m.containers["hello-web"].running = false }, "stopped"}, {func(m *machine) { m.containers["hello-web"].id = "another" }, "replaced"}, {func(m *machine) { delete(m.containers, "hello-web") }, "gone"}, } { dir, page, m := predecessor(t) d := adopted(t, untakenWeb, webResources(page)) _, state := applyAdopted(t, d, store.State{}, m, dir) c.change(m) m.asked = nil _, state = applyAdopted(t, d, state, m, dir) if h, _ := state.HeldAt("hello-web.server"); h.Changed != c.want { t.Errorf("%s: recorded as %q", c.want, h.Changed) } for _, a := range m.asked { if strings.HasPrefix(a, "docker run") || strings.HasPrefix(a, "docker rm") || strings.HasPrefix(a, "docker start") { t.Errorf("%s: the held container was acted on: %s", c.want, a) } } } } func TestAHeldFileThatVanishesIsNotCreated(t *testing.T) { dir, page, m := predecessor(t) d := adopted(t, untakenWeb, webResources(page)) _, state := applyAdopted(t, d, store.State{}, m, dir) if err := os.Remove(page); err != nil { t.Fatal(err) } _, state = applyAdopted(t, d, state, m, dir) if _, err := os.Stat(page); !errors.Is(err, os.ErrNotExist) { t.Fatal("a held file that vanished was created before its module was taken") } if h, _ := state.HeldAt("hello-web.page"); h.Changed != "gone" { t.Errorf("a vanished held file was not reported gone: %+v", h) } } func TestAConvergedNodeStillReplacesWhatItFinds(t *testing.T) { // No adoption, no holds: byte for byte what a converged node did before ADR 0100. dir, page, m := predecessor(t) d := parse(t, `{"declaration":1,"resources":[`+webResources(page)+`]}`) report, state := applyAdopted(t, d, store.State{}, m, dir) if got, _ := os.ReadFile(page); string(got) != "the mesh's page\n" { t.Errorf("a converged node kept a found file: %q", got) } if !m.removed("hello-web") { t.Error("a converged node kept a found container") } if len(state.Held) != 0 || outcomeOf(report, "hello-web.page").Action == "held" { t.Errorf("a converged node held something: %+v", state.Held) } if _, err := os.Stat(filepath.Join(dir, "kept")); !errors.Is(err, os.ErrNotExist) { t.Error("a converged node kept originals") } }