package apply import ( "context" "fmt" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/store" ) // Defends novox/hq ADR 0117: a service that omits its state leaves the unit's lifecycle to the // machine. The mesh reflects its triggers on a unit already running and does nothing else to it — // never starts, stops, enables or disables it, and forgets it when undeclared. // unitIn is a service manager whose one unit is active or not, recording what it is asked. func unitIn(active bool, commands *[]string) Runner { return func(_ context.Context, name string, args ...string) (string, error) { line := name + " " + strings.Join(args, " ") *commands = append(*commands, line) switch { case strings.Contains(line, "is-enabled"): return "enabled", nil case strings.Contains(line, "show") && strings.Contains(line, "ActiveState"): if active { return "LoadState=loaded\nActiveState=active\nSubState=running", nil } return "LoadState=loaded\nActiveState=inactive\nSubState=dead", nil } return "", nil } } func statelessDecl(path, content, triggers string) string { return fmt.Sprintf(`{"declaration":1,"resources":[ {"id":"uplink.conf","type":"file","path":%q,"into":"block","content":%q}, {"id":"uplink.manager","type":"service","unit":"NetworkManager.service",%s} ]}`, path, content, triggers) } // touched is whether any command would change the unit's lifecycle. func touched(commands []string) []string { var changing []string for _, c := range commands { for _, verb := range []string{" start ", " stop ", " restart ", " enable ", " disable ", " reload "} { if strings.Contains(c+" ", verb) { changing = append(changing, c) } } } return changing } func TestAStatelessServiceRunningIsReloadedForItsTrigger(t *testing.T) { path := filepath.Join(t.TempDir(), "mesh.conf") var commands []string report, _, err := Apply(context.Background(), archHost(t), parse(t, statelessDecl(path, "[main]\ndns=none\n", `"reload-on":["uplink.conf"]`)), store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil) if err != nil { t.Fatal(err) } joined := strings.Join(commands, "\n") if !strings.Contains(joined, "systemctl reload NetworkManager.service") { t.Errorf("the running manager was not reloaded; commands were %v", commands) } for _, c := range touched(commands) { if !strings.Contains(c, "reload") { t.Errorf("the manager's lifecycle was touched: %s", c) } } if o := outcomeOf(report, "uplink.manager"); o.Action != "updated" || !strings.Contains(o.Detail, "reloaded for uplink.conf") { t.Errorf("reported as %q: %s", o.Action, o.Detail) } } func TestAStatelessServiceNotRunningIsLeftSo(t *testing.T) { path := filepath.Join(t.TempDir(), "mesh.conf") for _, triggers := range []string{`"reload-on":["uplink.conf"]`, `"restart-on":["uplink.conf"]`} { var commands []string report, _, err := Apply(context.Background(), archHost(t), parse(t, statelessDecl(path, fmt.Sprintf("# %s\n", triggers), triggers)), store.State{}, store.OriginDeclared, unitIn(false, &commands), nil, nil) if err != nil { t.Fatal(err) } if changing := touched(commands); len(changing) > 0 { t.Errorf("%s: an inactive unit was acted on: %v", triggers, changing) } if o := outcomeOf(report, "uplink.manager"); o.Action != "unchanged" || o.Detail != "not running; the change applies at its next start" { t.Errorf("%s: reported as %q: %s", triggers, o.Action, o.Detail) } } } func TestAStatelessServiceIsRestartedOnlyForItsRestartTrigger(t *testing.T) { path := filepath.Join(t.TempDir(), "mesh.conf") var commands []string d := parse(t, statelessDecl(path, "x\n", `"restart-on":["uplink.conf"]`)) _, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil) if err != nil { t.Fatal(err) } if !strings.Contains(strings.Join(commands, "\n"), "stop NetworkManager.service") { t.Errorf("not restarted for its restart trigger; commands were %v", commands) } // Nothing it reflects changed: nothing is asked of the unit at all. commands = nil report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginDeclared, unitIn(true, &commands), nil, nil) if err != nil { t.Fatal(err) } if changing := touched(commands); len(changing) > 0 { t.Errorf("with nothing changed the unit was acted on: %v", changing) } if got := outcomeOf(report, "uplink.manager").Action; got != "unchanged" { t.Errorf("with nothing changed it was %q", got) } } func TestAStatelessServiceUndeclaredIsForgottenNotStopped(t *testing.T) { path := filepath.Join(t.TempDir(), "mesh.conf") var commands []string _, state, err := Apply(context.Background(), archHost(t), parse(t, statelessDecl(path, "x\n", `"reload-on":["uplink.conf"]`)), store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil) if err != nil { t.Fatal(err) } if rec, _ := state.Find("uplink.manager"); !rec.Stateless { t.Fatal("the record does not say the service was stateless") } if steps := Plan(somethingElse(t), state, store.OriginDeclared); !hasStep(steps, "forget", "uplink.manager") { t.Errorf("the preview does not forget it: %v", steps) } commands = nil report, _, err := Apply(context.Background(), archHost(t), somethingElse(t), state, store.OriginDeclared, unitIn(true, &commands), nil, nil) if err != nil { t.Fatal(err) } if changing := touched(commands); len(changing) > 0 { t.Errorf("undeclaring acted on the unit: %v", changing) } o := outcomeOf(report, "uplink.manager") if o.Action != "forgotten" || o.Detail != "its state was never the mesh's" { t.Errorf("undeclaring was %q: %s", o.Action, o.Detail) } } func TestAStatelessServiceIsNeverHeldOnAnAdoptedNode(t *testing.T) { path := filepath.Join(t.TempDir(), "mesh.conf") resources := fmt.Sprintf(`{"id":"uplink.conf","type":"file","path":%q,"into":"block","content":"x\n"}, {"id":"uplink.manager","type":"service","unit":"NetworkManager.service","reload-on":["uplink.conf"]}`, path) d := adopted(t, `{"taken":[],"untaken":{"uplink":["uplink.conf","uplink.manager"]}}`, resources) for _, s := range Plan(d, store.State{}, store.OriginDeclared) { if s.ID == "uplink.manager" && (s.Verb == "hold" || s.Verb == "create" || strings.Contains(s.Why, "replaces")) { t.Errorf("the preview holds or replaces a stateless service: %+v", s) } } var commands []string report, state, err := ApplyKeeping(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil, KeepIn(t.TempDir())) if err != nil { t.Fatal(err) } if got := outcomeOf(report, "uplink.manager").Action; got == "held" { t.Fatal("a stateless service was held, though it replaces nothing that was found") } if len(state.Held) != 0 { t.Errorf("something was held: %+v", state.Held) } for _, c := range touched(commands) { if !strings.Contains(c, "reload") { t.Errorf("the adopted node's manager lifecycle was touched: %s", c) } } } func hasStep(steps []Step, verb, id string) bool { for _, s := range steps { if s.Verb == verb && s.ID == id { return true } } return false }