Hold a found directory, a found service's unit, a container that would mount found data, and a step run in a held container on an adopted node (hq ADR 0103)
This commit is contained in:
+244
-60
@@ -19,6 +19,54 @@ import (
|
||||
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 {
|
||||
@@ -29,7 +77,18 @@ type fakeContainer struct {
|
||||
|
||||
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":
|
||||
@@ -278,88 +337,213 @@ func TestAHeldFileIsNeverRemovedWhenItsModuleIsUnassigned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHeldFileRewrittenIsReportedAndNotReverted(t *testing.T) {
|
||||
dir, page, m := predecessor(t)
|
||||
d := adopted(t, untakenWeb, webResources(page))
|
||||
_, state := applyAdopted(t, d, store.State{}, m, dir)
|
||||
// Defends novox/hq ADR 0103: found covers every kind that can reach what the machine already has.
|
||||
|
||||
if err := os.WriteFile(page, []byte("the predecessor wrote again\n"), 0o640); err != nil {
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
inside := filepath.Join(data, "PG_VERSION")
|
||||
if err := os.WriteFile(inside, []byte("16\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h, _ := state.HeldAt("hello-web.page"); h.Changed != "rewritten" || h.ChangedAt.IsZero() {
|
||||
t.Errorf("a rewrite was not recorded: %+v", h)
|
||||
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 o := outcomeOf(report, "hello-web.page"); !strings.Contains(o.Detail, "rewritten") {
|
||||
t.Errorf("a rewrite was not reported: %+v", o)
|
||||
if got, _ := os.ReadFile(inside); string(got) != "16\n" {
|
||||
t.Errorf("what is inside a found directory was touched: %q", got)
|
||||
}
|
||||
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)
|
||||
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 TestAHeldContainerStoppedOrReplacedIsReportedAndNotRestarted(t *testing.T) {
|
||||
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 {
|
||||
change func(*machine)
|
||||
want string
|
||||
name, volume string
|
||||
m *machine
|
||||
}{
|
||||
{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"},
|
||||
{"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}}},
|
||||
} {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 TestAHeldFileThatVanishesIsNotCreated(t *testing.T) {
|
||||
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))
|
||||
_, 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)+`]}`)
|
||||
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 got, _ := os.ReadFile(page); string(got) != "the mesh's page\n" {
|
||||
t.Errorf("a converged node kept a found file: %q", got)
|
||||
if m.did("docker exec") {
|
||||
t.Errorf("an action was run inside a held container: %v", m.asked)
|
||||
}
|
||||
if !m.removed("hello-web") {
|
||||
t.Error("a converged node kept a found container")
|
||||
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 len(state.Held) != 0 || outcomeOf(report, "hello-web.page").Action == "held" {
|
||||
t.Errorf("a converged node held something: %+v", state.Held)
|
||||
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)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "kept")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Error("a converged node kept originals")
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user