Files
mesh-host/internal/apply/stateless_test.go
T
jochen a018ebd1ca
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Pass -- before every unit, and give a unit back only when it is exactly as the mesh held it
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 `--`.
2026-10-09 01:54:26 +02:00

193 lines
7.1 KiB
Go

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) {
args = oldShape(name, args)
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
}