Third review of mesh-catalog #147. A unit's name can now come from a setting (a template's instance), so every systemctl call that names a unit passes `--` before it, and a name can never be read as an option. On removal, a unit is started or enabled again only when it is exactly the stopped and disabled the mesh held it to. A masked unit, or one somebody started or enabled since, is left as it is, and the report says why. A restore that fails (the start refused, or the unit not running after it) puts back what this attempt changed and is reported as waiting, so no apply is made fatal by it and the next one tries again. The tests' fake service managers read systemctl's arguments in the shape they were written for (oldShape); internal/system's own test holds the `--`.
171 lines
6.8 KiB
Go
171 lines
6.8 KiB
Go
package apply
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// A unit the mesh found running and enabled, and held stopped and disabled by its own declaration, is
|
|
// given back running and enabled when that declaration goes (the zfs module holds the distribution's
|
|
// scrub timer stopped while its own timer scrubs the pool; unassigning it must not leave the pool with no
|
|
// schedule at all). The report says so.
|
|
func TestAUnitTheMeshStoppedAndDisabledIsGivenBackRunningAndEnabled(t *testing.T) {
|
|
m := unitsMachine(map[string]*fakeUnit{
|
|
"zfs-scrub-weekly@storage.timer": {active: "active", enabled: "enabled"},
|
|
})
|
|
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
|
{"id":"zfs.distribution-weekly","type":"service","unit":"zfs-scrub-weekly@storage.timer","state":"stopped","boot":"disabled"}]}`,
|
|
store.State{}, m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if u := m.units["zfs-scrub-weekly@storage.timer"]; u.active != "inactive" || u.enabled != "disabled" {
|
|
t.Fatalf("the fixture did not stop and disable it: %s and %s", u.active, u.enabled)
|
|
}
|
|
report, _, err := applyOn(t, nothingButA(t), copyOf(t, state), m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if u := m.units["zfs-scrub-weekly@storage.timer"]; u.active != "active" || u.enabled != "enabled" {
|
|
t.Errorf("given back as %s and %s; it was found running and enabled", u.active, u.enabled)
|
|
}
|
|
said := ""
|
|
for _, r := range report.Outcomes {
|
|
if r.ID == "zfs.distribution-weekly" {
|
|
said = r.Action + ": " + r.Detail
|
|
}
|
|
}
|
|
if !strings.HasPrefix(said, "restored: ") || !strings.Contains(said, "started") || !strings.Contains(said, "enabled at boot") {
|
|
t.Errorf("the report says %q", said)
|
|
}
|
|
}
|
|
|
|
// Only what the mesh's own declaration did is undone: a unit the mesh held stopped that was found stopped
|
|
// stays stopped, and one found running that the mesh never stopped is left running as before.
|
|
func TestOnlyWhatTheMeshsDeclarationStoppedIsStartedAgain(t *testing.T) {
|
|
m := unitsMachine(map[string]*fakeUnit{
|
|
"found-stopped.timer": {active: "inactive", enabled: "disabled"},
|
|
"left-running.timer": {active: "active", enabled: "enabled"},
|
|
})
|
|
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
|
{"id":"a","type":"service","unit":"found-stopped.timer","state":"stopped","boot":"disabled"},
|
|
{"id":"b","type":"service","unit":"left-running.timer","state":"running","boot":"enabled"}]}`,
|
|
store.State{}, m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := applyOn(t, nothingButA(t), state, m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if u := m.units["found-stopped.timer"]; u.active != "inactive" || u.enabled != "disabled" {
|
|
t.Errorf("found stopped and disabled, given back as %s and %s", u.active, u.enabled)
|
|
}
|
|
if u := m.units["left-running.timer"]; u.active != "active" || u.enabled != "enabled" {
|
|
t.Errorf("found running, given back as %s and %s", u.active, u.enabled)
|
|
}
|
|
}
|
|
|
|
// A record from before the host kept what it declared says nothing about who stopped the unit: it is
|
|
// left as it is, never started.
|
|
func TestARecordWithoutWhatWasDeclaredStartsNothing(t *testing.T) {
|
|
m := unitsMachine(map[string]*fakeUnit{"old.timer": {active: "inactive", enabled: "disabled"}})
|
|
state := store.State{}
|
|
state.Record(store.Applied{ID: "old", Type: "service", Target: "old.timer", Origin: store.OriginDeclared,
|
|
Found: &store.FoundUnit{Unit: "old.timer", State: "running", Boot: "enabled"}})
|
|
if _, _, err := applyOn(t, nothingButA(t), state, m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if u := m.units["old.timer"]; u.active != "inactive" || u.enabled != "disabled" {
|
|
t.Errorf("an old record started %s and %s", u.active, u.enabled)
|
|
}
|
|
}
|
|
|
|
// The plan says it before the apply does it.
|
|
func TestThePlanSaysAStoppedUnitIsStartedAgain(t *testing.T) {
|
|
m := unitsMachine(map[string]*fakeUnit{"zfs-scrub-weekly@storage.timer": {active: "active", enabled: "enabled"}})
|
|
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
|
{"id":"w","type":"service","unit":"zfs-scrub-weekly@storage.timer","state":"stopped","boot":"disabled"}]}`,
|
|
store.State{}, m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
said := ""
|
|
for _, s := range Plan(parse(t, nothingButA(t)), state, store.OriginDeclared) {
|
|
if s.ID == "w" {
|
|
said = s.Verb + ": " + s.Why
|
|
}
|
|
}
|
|
if !strings.HasPrefix(said, "restore: ") || !strings.Contains(said, "started and enabled at boot again") {
|
|
t.Errorf("the plan says %q", said)
|
|
}
|
|
}
|
|
|
|
// Third review: on removal a unit is started again only when it is exactly as the mesh held it, stopped and
|
|
// disabled. A masked unit, or one somebody started or enabled since, is left alone, and the report says why.
|
|
func TestAMaskedOrChangedUnitIsLeftAloneOnRemoval(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name, active, enabled, says string
|
|
}{
|
|
{"masked", "inactive", "masked", "masked"},
|
|
{"enabled since", "inactive", "enabled", "not the stopped and disabled"},
|
|
{"started since", "active", "disabled", "not the stopped and disabled"},
|
|
} {
|
|
m := unitsMachine(map[string]*fakeUnit{"w.timer": {active: "active", enabled: "enabled"}})
|
|
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
|
{"id":"w","type":"service","unit":"w.timer","state":"stopped","boot":"disabled"}]}`, store.State{}, m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.units["w.timer"].active, m.units["w.timer"].enabled = c.active, c.enabled
|
|
report, _, err := applyOn(t, nothingButA(t), state, m)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", c.name, err)
|
|
}
|
|
if u := m.units["w.timer"]; u.active != c.active || u.enabled != c.enabled {
|
|
t.Errorf("%s: changed to %s and %s", c.name, u.active, u.enabled)
|
|
}
|
|
said := ""
|
|
for _, o := range report.Outcomes {
|
|
if o.ID == "w" {
|
|
said = o.Action + ": " + o.Detail
|
|
}
|
|
}
|
|
if !strings.HasPrefix(said, "forgotten: ") || !strings.Contains(said, c.says) {
|
|
t.Errorf("%s: the report says %q", c.name, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A restore that fails — the start refused, or the unit not running after it — waits: the record stays,
|
|
// the report says so, and no apply is made fatal by it.
|
|
func TestAFailedRestoreWaitsAndIsNeverFatal(t *testing.T) {
|
|
m := unitsMachine(map[string]*fakeUnit{"w.timer": {active: "active", enabled: "enabled"}})
|
|
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
|
{"id":"w","type":"service","unit":"w.timer","state":"stopped","boot":"disabled"}]}`, store.State{}, m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.units["w.timer"].wontStart = true
|
|
for i := 0; i < 2; i++ {
|
|
report, next, err := applyOn(t, nothingButA(t), state, m)
|
|
if err != nil {
|
|
t.Fatalf("apply %d failed: %v", i+1, err)
|
|
}
|
|
said := ""
|
|
for _, o := range report.Outcomes {
|
|
if o.ID == "w" {
|
|
said = o.Action + ": " + o.Detail
|
|
}
|
|
}
|
|
if !strings.Contains(said, "wait") || !strings.Contains(said, "not running") {
|
|
t.Errorf("apply %d: the report says %q", i+1, said)
|
|
}
|
|
if _, kept := next.Find("w"); !kept {
|
|
t.Errorf("apply %d: the record went, so nothing would try again", i+1)
|
|
}
|
|
state = next
|
|
}
|
|
}
|