Files
mesh-host/internal/apply/hold_test.go
T

550 lines
21 KiB
Go

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
// units are service units by name, as systemd would report them; volumes are the runtime's
// named volumes.
units map[string]*fakeUnit
volumes map[string]bool
}
type fakeUnit struct {
active, enabled string
}
// systemctl answers as systemd does for the units the machine has, and "not-found" for any other.
func (m *machine) systemctl(args []string) (string, error) {
unit := args[len(args)-1]
if args[0] == "show" {
unit = args[1]
}
u, ok := m.units[unit]
switch args[0] {
case "show":
if !ok {
return "LoadState=not-found\nActiveState=inactive\nType=simple\n", nil
}
return "LoadState=loaded\nActiveState=" + u.active + "\nType=simple\nRemainAfterExit=no\n", nil
case "is-enabled":
if !ok {
return "", errors.New("not found")
}
return u.enabled + "\n", nil
case "start":
u.active = "active"
case "stop":
u.active = "inactive"
case "enable":
u.enabled = "enabled"
case "disable":
u.enabled = "disabled"
}
return "", nil
}
func (m *machine) did(prefix string) bool {
for _, a := range m.asked {
if strings.HasPrefix(a, prefix) {
return true
}
}
return false
}
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, " "))
if name == "systemctl" {
return m.systemctl(args)
}
if name != "docker" {
return "", nil
}
switch args[0] {
case "volume":
if m.volumes[args[len(args)-1]] {
return "[]\n", nil
}
return "", errors.New("no such volume")
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")
}
}
// Defends novox/hq ADR 0103: found covers every kind that can reach what the machine already has.
// untaken is an adoption with hello-web untaken, listing these of its resources.
func untaken(ids ...string) string {
return `{"taken":[],"untaken":{"hello-web":["` + strings.Join(ids, `","`) + `"]}}`
}
func TestAFoundDirectoryOfAnUntakenModuleKeepsItsModeOwnerAndContents(t *testing.T) {
dir := t.TempDir()
data := filepath.Join(dir, "data")
if err := os.Mkdir(data, 0o700); err != nil {
t.Fatal(err)
}
inside := filepath.Join(data, "PG_VERSION")
if err := os.WriteFile(inside, []byte("16\n"), 0o600); err != nil {
t.Fatal(err)
}
m := &machine{containers: map[string]*fakeContainer{}}
report, state := applyAdopted(t, adopted(t, untaken("hello-web.data"),
`{"id":"hello-web.data","type":"directory","path":"`+data+`","mode":"0755"}`), store.State{}, m, dir)
if info, _ := os.Stat(data); info.Mode().Perm() != 0o700 {
t.Errorf("a found directory was re-moded to %o", info.Mode().Perm())
}
if got, _ := os.ReadFile(inside); string(got) != "16\n" {
t.Errorf("what is inside a found directory was touched: %q", got)
}
if o := outcomeOf(report, "hello-web.data"); o.Action != "held" || !strings.Contains(o.Detail, "mode, owner and contents") {
t.Errorf("the found directory was not held: %+v", o)
}
if h, ok := state.HeldAt("hello-web.data"); !ok || h.Mode != "0700" {
t.Errorf("the directory as found was not recorded: %+v", h)
}
if _, recorded := state.Find("hello-web.data"); recorded {
t.Error("a held directory was recorded as applied")
}
}
func TestADirectoryMadeInTheSameApplyIsNotFound(t *testing.T) {
// A file's parent is made as the file is written; what the mesh made is not found.
dir := t.TempDir()
data := filepath.Join(dir, "data")
m := &machine{containers: map[string]*fakeContainer{}}
report, state := applyAdopted(t, adopted(t, untaken("hello-web.conf", "hello-web.data"),
`{"id":"hello-web.conf","type":"file","path":"`+filepath.Join(data, "conf")+`","content":"x\n"},
{"id":"hello-web.data","type":"directory","path":"`+data+`","mode":"0750"}`), store.State{}, m, dir)
if o := outcomeOf(report, "hello-web.data"); o.Action == "held" {
t.Errorf("a directory the apply itself made was held: %+v", o)
}
if info, _ := os.Stat(data); info.Mode().Perm() != 0o750 {
t.Errorf("the mesh's own directory was not converged: %o", info.Mode().Perm())
}
if len(state.Held) != 0 {
t.Errorf("held: %+v", state.Held)
}
}
func TestAFoundServiceOfAnUntakenModuleIsNeitherStartedNorEnabledNorRestarted(t *testing.T) {
dir := t.TempDir()
conf := filepath.Join(dir, "hello.conf")
m := &machine{containers: map[string]*fakeContainer{},
units: map[string]*fakeUnit{"hello.service": {active: "inactive", enabled: "disabled"}}}
report, state := applyAdopted(t, adopted(t, untaken("hello-web.conf", "hello-web.unit"),
`{"id":"hello-web.conf","type":"file","path":"`+conf+`","content":"x\n"},
{"id":"hello-web.unit","type":"service","unit":"hello.service","state":"running","boot":"enabled",
"restart-on":["hello-web.conf"]}`), store.State{}, m, dir)
for _, verb := range []string{"systemctl start", "systemctl stop", "systemctl enable", "systemctl disable", "systemctl restart"} {
if m.did(verb) {
t.Errorf("a found service was changed: %s (%v)", verb, m.asked)
}
}
if o := outcomeOf(report, "hello-web.unit"); o.Action != "held" || !strings.Contains(o.Detail, "starts at boot") {
t.Errorf("the found service was not held: %+v", o)
}
if h, ok := state.HeldAt("hello-web.unit"); !ok || h.Running {
t.Errorf("the service as found was not recorded: %+v", h)
}
}
func TestAHeldServiceIsStillReloadedButNeverRestarted(t *testing.T) {
// A reload stops nothing (novox/hq ADR 0102); a restart would stop the predecessor's service.
dir := t.TempDir()
conf := filepath.Join(dir, "daemon.json")
m := &machine{containers: map[string]*fakeContainer{},
units: map[string]*fakeUnit{"docker.service": {active: "active", enabled: "enabled"}}}
report, _ := applyAdopted(t, adopted(t, untaken("hello-web.conf", "hello-web.unit"),
`{"id":"hello-web.conf","type":"file","path":"`+conf+`","content":"{}\n"},
{"id":"hello-web.unit","type":"service","unit":"docker.service","state":"running","boot":"enabled",
"reload-on":["hello-web.conf"]}`), store.State{}, m, dir)
if !m.did("systemctl reload docker.service") {
t.Errorf("a held service was not reloaded for what it re-reads: %v", m.asked)
}
if m.did("systemctl stop") || m.did("systemctl start") {
t.Errorf("a held service was restarted: %v", m.asked)
}
if o := outcomeOf(report, "hello-web.unit"); o.Action != "held" || !strings.Contains(o.Detail, "reloaded for hello-web.conf") {
t.Errorf("the reload was not reported on the hold: %+v", o)
}
}
func TestAServiceWhoseUnitIsNotThereIsAppliedAsUsual(t *testing.T) {
// No unit before the apply: nothing of a predecessor's to hold.
dir := t.TempDir()
m := &machine{containers: map[string]*fakeContainer{}, units: map[string]*fakeUnit{}}
d := adopted(t, untaken("hello-web.unit"), `{"id":"hello-web.unit","type":"service","unit":"hello.service","state":"running"}`)
_, _, err := ApplyKeeping(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared,
m.run, nil, nil, KeepIn(dir))
if err == nil || !strings.Contains(err.Error(), "does not exist") {
t.Errorf("an absent unit was held rather than applied: %v", err)
}
}
func TestAContainerThatWouldMountFoundDataIsNotCreated(t *testing.T) {
dir := t.TempDir()
data := filepath.Join(dir, "predecessor-data")
if err := os.Mkdir(data, 0o700); err != nil {
t.Fatal(err)
}
for _, c := range []struct {
name, volume string
m *machine
}{
{"a path", data + ":/var/lib/postgresql/data", &machine{containers: map[string]*fakeContainer{}}},
{"a named volume", "predecessor-pgdata:/var/lib/postgresql/data",
&machine{containers: map[string]*fakeContainer{}, volumes: map[string]bool{"predecessor-pgdata": true}}},
} {
report, state := applyAdopted(t, adopted(t, untaken("hello-web.server"),
`{"id":"hello-web.server","type":"container","name":"hello-web","image":"`+pinned+`",
"volumes":["`+c.volume+`"]}`), store.State{}, c.m, dir)
if c.m.did("docker run") {
t.Errorf("%s: a container mounting found data was created: %v", c.name, c.m.asked)
}
o := outcomeOf(report, "hello-web.server")
if o.Action != "held" || !strings.Contains(o.Detail, "would mount") {
t.Errorf("%s: not held: %+v", c.name, o)
}
if h, ok := state.HeldAt("hello-web.server"); !ok || !strings.Contains(h.Why, "would mount") {
t.Errorf("%s: the hold does not say why: %+v", c.name, h)
}
// Held, it stays held on the next pass, and is still not created.
c.m.asked = nil
_, state = applyAdopted(t, adopted(t, untaken("hello-web.server"),
`{"id":"hello-web.server","type":"container","name":"hello-web","image":"`+pinned+`",
"volumes":["`+c.volume+`"]}`), state, c.m, dir)
if c.m.did("docker run") || len(state.Held) != 1 {
t.Errorf("%s: a held container was created on the next pass: %v", c.name, c.m.asked)
}
}
}
func TestAContainerMountingWhatTheMeshMadeIsCreated(t *testing.T) {
dir := t.TempDir()
data := filepath.Join(dir, "data")
m := &machine{containers: map[string]*fakeContainer{}}
report, _ := applyAdopted(t, adopted(t, untaken("hello-web.data", "hello-web.server"),
`{"id":"hello-web.data","type":"directory","path":"`+data+`"},
{"id":"hello-web.server","type":"container","name":"hello-web","image":"`+pinned+`",
"volumes":["`+data+`:/data"]}`), store.State{}, m, dir)
if o := outcomeOf(report, "hello-web.server"); o.Action != "created" {
t.Errorf("a container mounting only what the mesh made was not created: %+v", o)
}
}
func TestARunOnceStepInAHeldContainerIsHeld(t *testing.T) {
dir, page, m := predecessor(t)
step := `{"id":"hello-web.migrate","type":"container","name":"hello-web-migrate","image":"` + pinned + `",
"run-once":true,"network":"container:hello-web"}`
report, state := applyAdopted(t, adopted(t, untaken("hello-web.page", "hello-web.server", "hello-web.migrate"), webResources(page)+","+step), store.State{}, m, dir)
if m.did("docker run") {
t.Errorf("a step was run inside a held container: %v", m.asked)
}
o := outcomeOf(report, "hello-web.migrate")
if o.Action != "held" || !strings.Contains(o.Detail, "runs in hello-web") {
t.Errorf("the step was not held: %+v", o)
}
if _, ok := state.HeldAt("hello-web.migrate"); !ok {
t.Error("the held step is not reported held")
}
}
func TestAnActionInAHeldContainerIsHeldUntilItsModuleIsTaken(t *testing.T) {
// An action cannot arrive over the link today, and a bundle cannot say a node is adopted; the
// host holds one anyway, since what it would run in is the predecessor's.
dir, page, m := predecessor(t)
d := adopted(t, untakenWeb, webResources(page))
d.Resources = append(d.Resources, &declaration.Action{ID: "hello-web.seed", Type: declaration.TypeAction,
In: "hello-web", Command: []string{"seed"}, Verify: []string{"seeded"}})
report, state := applyAdopted(t, d, store.State{}, m, dir)
if m.did("docker exec") {
t.Errorf("an action was run inside a held container: %v", m.asked)
}
if o := outcomeOf(report, "hello-web.seed"); o.Action != "held" || !strings.Contains(o.Detail, "not run until hello-web is taken") {
t.Errorf("the action was not held: %+v", o)
}
if h, ok := state.HeldAt("hello-web.seed"); !ok || h.Module != "hello-web" {
t.Errorf("the held action is not its module's: %+v", h)
}
// Taken: the container is the mesh's, and the action runs in it.
taken := adopted(t, takenWeb, webResources(page))
taken.Resources = append(taken.Resources, d.Resources[len(d.Resources)-1])
report, state = applyAdopted(t, taken, state, m, dir)
if !m.did("docker exec hello-web ") {
t.Errorf("the action did not run once its module was taken: %v", m.asked)
}
if _, still := state.HeldAt("hello-web.seed"); still || len(state.Held) != 0 {
t.Errorf("holds outlived the take: %+v", state.Held)
}
}