package apply import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/store" "github.com/novox/mesh-host/internal/system" ) // Defends novox/hq ADR 0177: a unit in the operator account's own service manager is applied // through that manager — `systemctl --user --machine=@` — and never as a system unit of // the same name; its record remembers the scope so removal goes the same way. The account's // manager runs only while somebody is logged in or the account lingers: with it away a unit waits // rather than fails, a removal is never fatal, and the manager is never started by asking it. // account is a machine with one account whose own manager runs or does not, and the units in it. type account struct { name, uid, home string // up is whether the account's manager runs: a login, or lingering. up bool // lingers is logind's record, kept as files in a directory a test owns. lingerDir string // units are the account's units by name; system are the machine's. units, system map[string]*fakeUnit // busDown is a manager that says it runs while its bus does not answer — it stopped between // the question and the command. busDown bool asked []string // strays are system-scope commands that reached a unit, which no test here expects unless it // declares a system unit. strays []string } func newAccount(t *testing.T, up bool) *account { t.Helper() was := serviceSettle serviceSettle = 0 dir := t.TempDir() restore := system.LingerIn(dir) t.Cleanup(func() { serviceSettle = was; restore() }) return &account{name: "ops", uid: "1001", home: "/home/ops", up: up, lingerDir: dir, units: map[string]*fakeUnit{"i3-reload-watcher.service": {active: "inactive", enabled: "disabled"}}, system: map[string]*fakeUnit{}} } func (a *account) did(prefix string) bool { for _, c := range a.asked { if strings.HasPrefix(c, prefix) { return true } } return false } func (a *account) run(_ context.Context, name string, args ...string) (string, error) { line := name + " " + strings.Join(args, " ") a.asked = append(a.asked, line) switch name { case "getent": if args[len(args)-1] == a.name { return a.name + ":x:" + a.uid + ":" + a.uid + "::" + a.home + ":/bin/bash\n", nil } return "", errors.New("getent exited 2: ") case "loginctl": path := filepath.Join(a.lingerDir, args[1]) switch args[0] { case "enable-linger": a.up = true return "", os.WriteFile(path, nil, 0o644) case "disable-linger": a.up = false return "", os.Remove(path) } case "systemctl": if line == "systemctl is-active user@"+a.uid+".service" { if a.up { return "active\n", nil } return "inactive\n", errors.New("systemctl exited 3: ") } prefix := "--machine=" + a.name + "@" if len(args) > 1 && args[0] == "--user" && args[1] == prefix { if !a.up || a.busDown { return "", errors.New("systemctl exited 1: Failed to connect to user scope bus via " + "machine transport: No such file or directory") } return unitCommand(a.units, args[2:]) } a.strays = append(a.strays, line) return unitCommand(a.system, args) } return "", nil } // unitCommand is one systemctl verb against a set of units. func unitCommand(units map[string]*fakeUnit, args []string) (string, error) { if len(args) == 0 { return "", nil } if args[0] == "daemon-reload" { return "", nil } u, ok := units[args[1]] switch args[0] { case "show": if !ok { return "LoadState=not-found\nActiveState=inactive", nil } return "LoadState=loaded\nActiveState=" + u.active, nil case "is-enabled": if !ok { return "", errors.New("systemctl exited 1: ") } return u.enabled + "\n", nil } if !ok { return "", fmt.Errorf("systemctl exited 5: Unit %s not found", args[1]) } switch args[0] { case "start", "restart": u.active = "active" case "stop": u.active = "inactive" case "enable": u.enabled = "enabled" case "disable": u.enabled = "disabled" } return "", nil } const watcher = `{"id":"i3.watcher","type":"service","unit":"i3-reload-watcher.service","state":"running","boot":"enabled","scope":"user","user":"ops"}` func declaring(resources ...string) string { return `{"declaration":1,"resources":[` + strings.Join(resources, ",") + `]}` } func applyAccount(t *testing.T, raw string, known store.State, a *account) (Report, store.State, error) { t.Helper() return Apply(context.Background(), archHost(t), parse(t, raw), known, store.OriginDeclared, a.run, nil, nil) } func outcomeFor(r Report, id string) Outcome { for _, o := range r.Outcomes { if o.ID == id { return o } } return Outcome{} } func TestAUserScopedUnitIsAppliedThroughTheAccountsManager(t *testing.T) { a := newAccount(t, true) report, known, err := applyAccount(t, declaring(watcher), store.State{}, a) if err != nil { t.Fatalf("apply: %v\n%s", err, strings.Join(a.asked, "\n")) } if !report.Changed() { t.Fatal("a unit that was stopped and is now running changed nothing") } if !a.did("systemctl --user --machine=ops@ start i3-reload-watcher.service") || !a.did("systemctl --user --machine=ops@ enable i3-reload-watcher.service") { t.Fatalf("the unit was not started and enabled in the account's manager:\n%s", strings.Join(a.asked, "\n")) } if len(a.strays) != 0 { t.Fatalf("a user-scoped unit reached the machine's manager: %v", a.strays) } recorded, ok := known.At("service", "i3-reload-watcher.service") if !ok || recorded.Scope != "user" || recorded.User != "ops" || recorded.Found == nil { t.Fatalf("the record does not say whose manager the unit is in, or what was found: %+v", recorded) } } func TestASystemUnitIsUntouchedByTheScope(t *testing.T) { var commands []string decl := `{"declaration":1,"resources":[ {"id":"x.daemon","type":"service","unit":"sshd.service","state":"running","boot":"enabled"} ]}` if _, _, err := Apply(context.Background(), archHost(t), parse(t, decl), store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil); err != nil { t.Fatal(err) } for _, c := range commands { if strings.Contains(c, "--user") || strings.Contains(c, "--machine") || strings.Contains(c, "user@") { t.Fatalf("a system unit was addressed to an account's manager: %s", c) } } } // With nobody logged in and no lingering, the unit waits: not a failure, nothing recorded, and the // account's manager never asked — asking it would log the account in. func TestAUserUnitWaitsForItsAccountsManagerAndIsAppliedWhenItRuns(t *testing.T) { a := newAccount(t, false) report, known, err := applyAccount(t, declaring(watcher), store.State{}, a) if err != nil { t.Fatalf("a unit whose account is not logged in failed the apply: %v", err) } o := outcomeFor(report, "i3.watcher") if o.Action != "waiting" || !strings.Contains(o.Detail, "not running") { t.Fatalf("the outcome does not say the unit waits for its manager: %+v", o) } if report.Changed() { t.Error("a unit that waited is reported as a change") } if a.did("systemctl --user") { t.Fatalf("the account's manager was asked while it was not running:\n%s", strings.Join(a.asked, "\n")) } if _, ok := known.Find("i3.watcher"); ok { t.Fatal("a unit never applied was recorded") } // The person logs in. a.up = true report, known, err = applyAccount(t, declaring(watcher), known, a) if err != nil { t.Fatal(err) } if o := outcomeFor(report, "i3.watcher"); o.Action == "waiting" || a.units["i3-reload-watcher.service"].active != "active" { t.Fatalf("the unit was not applied once its manager ran: %+v", o) } if _, ok := known.Find("i3.watcher"); !ok { t.Fatal("the unit was applied and not recorded") } } // A unit applied before, its account since logged out: the record stays exactly as it was, what // was found included, so a later removal still gives back what was there before the mesh. func TestAWaitingUnitKeepsItsRecord(t *testing.T) { a := newAccount(t, true) _, known, err := applyAccount(t, declaring(watcher), store.State{}, a) if err != nil { t.Fatal(err) } before, _ := known.Find("i3.watcher") a.up = false _, known, err = applyAccount(t, declaring(watcher), known, a) if err != nil { t.Fatal(err) } after, ok := known.Find("i3.watcher") if !ok || after.Found == nil || *after.Found != *before.Found || after.Scope != "user" { t.Fatalf("waiting changed the record: before %+v, after %+v", before, after) } } // A manager that says it runs and then does not answer — the person logged out mid-apply — is the // same absence, and is said the same way. func TestAManagerThatStopsDuringTheApplyIsWaitedFor(t *testing.T) { a := newAccount(t, true) a.busDown = true calls := 0 run := func(ctx context.Context, name string, args ...string) (string, error) { if name == "systemctl" && len(args) == 2 && args[0] == "is-active" { calls++ if calls > 1 { a.up = false } } return a.run(ctx, name, args...) } report, _, err := Apply(context.Background(), archHost(t), parse(t, declaring(watcher)), store.State{}, store.OriginDeclared, run, nil, nil) if err != nil { t.Fatalf("a manager that went away mid-apply failed it: %v", err) } if o := outcomeFor(report, "i3.watcher"); o.Action != "waiting" { t.Fatalf("not waiting: %+v", o) } } func TestAUserUnitOfAnAccountTheMachineDoesNotHaveIsRefused(t *testing.T) { a := newAccount(t, true) a.name = "someone-else" _, _, err := applyAccount(t, declaring(watcher), store.State{}, a) if err == nil || !strings.Contains(err.Error(), `"ops"`) { t.Fatalf("a unit of an account that is not here was not refused naming it: %v", err) } } // Without a manager per account — OpenRC — a user-scoped unit would otherwise be applied as the // machine's service of the same name. Refused instead. func TestAUserUnitIsRefusedWhereTheServiceManagerHasNoAccounts(t *testing.T) { alpine, err := system.For("alpine") if err != nil { t.Fatal(err) } a := newAccount(t, true) _, _, err = Apply(context.Background(), alpine, parse(t, declaring(watcher)), store.State{}, store.OriginDeclared, a.run, nil, nil) if err == nil || !strings.Contains(err.Error(), "no manager per account") { t.Fatalf("not refused: %v", err) } if a.did("rc-service") { t.Fatalf("a user-scoped unit reached the machine's services: %v", a.asked) } } // **Removal is never fatal** (the issue 162/228 wedge): with the manager away the record stays and // the outcome says it waits; the first apply that finds the manager gives the unit back. func TestRemovingAUserUnitWithItsManagerAwayWaitsAndIsNeverFatal(t *testing.T) { a := newAccount(t, true) _, known, err := applyAccount(t, declaring(watcher), store.State{}, a) if err != nil { t.Fatal(err) } a.up = false report, known, err := applyAccount(t, nothingButA(t), known, a) if err != nil { t.Fatalf("undeclaring a unit whose account is logged out failed the apply: %v", err) } if o := outcomeFor(report, "i3.watcher"); o.Action != "waiting" || !strings.Contains(o.Detail, "not running") { t.Fatalf("the removal does not say it waits: %+v", o) } if _, ok := known.Find("i3.watcher"); !ok { t.Fatal("a unit not given back was forgotten") } a.up = true report, known, err = applyAccount(t, nothingButA(t), known, a) if err != nil { t.Fatal(err) } u := a.units["i3-reload-watcher.service"] if u.active != "inactive" || u.enabled != "disabled" { t.Fatalf("the unit was not given back as found: %+v (%+v)", u, outcomeFor(report, "i3.watcher")) } if _, ok := known.Find("i3.watcher"); ok { t.Fatal("a unit given back is still recorded") } if len(a.strays) != 0 { t.Fatalf("the removal reached the machine's manager: %v", a.strays) } } // "Failed to connect to bus" from a manager that said it ran is said and retried, never fatal. func TestRemovingAUserUnitWhoseBusDoesNotAnswerIsNotFatal(t *testing.T) { a := newAccount(t, true) _, known, err := applyAccount(t, declaring(watcher), store.State{}, a) if err != nil { t.Fatal(err) } a.busDown = true report, known, err := applyAccount(t, nothingButA(t), known, a) if err != nil { t.Fatalf("a bus that did not answer made the removal fatal: %v", err) } if o := outcomeFor(report, "i3.watcher"); o.Action != "waiting" || !strings.Contains(o.Detail, "Failed to connect") { t.Fatalf("the removal does not say what stopped it: %+v", o) } if _, ok := known.Find("i3.watcher"); !ok { t.Fatal("a unit not given back was forgotten") } } func TestRemovingAUserUnitOfAnAccountThatIsGoneForgetsIt(t *testing.T) { a := newAccount(t, true) _, known, err := applyAccount(t, declaring(watcher), store.State{}, a) if err != nil { t.Fatal(err) } a.name = "renamed" report, known, err := applyAccount(t, nothingButA(t), known, a) if err != nil { t.Fatal(err) } if o := outcomeFor(report, "i3.watcher"); o.Action != "forgotten" || !strings.Contains(o.Detail, "no longer on this machine") { t.Fatalf("%+v", o) } if _, ok := known.Find("i3.watcher"); ok { t.Fatal("still recorded") } } // A unit file the mesh wrote under the account's own unit directory makes the unit the mesh's — and // only the account's unit of that name, never the machine's. func TestAUserUnitsFileTheMeshWroteMakesThatUnitAndNoOtherTheMeshs(t *testing.T) { home := t.TempDir() was := homeOf homeOf = func(name string) (string, error) { if name == "ops" { return home, nil } return "", errors.New("no such account") } t.Cleanup(func() { homeOf = was }) known := store.State{Resources: []store.Applied{ {ID: "f", Type: "file", Target: filepath.Join(home, ".config/systemd/user/watcher.service")}, {ID: "g", Type: "file", Target: "/etc/systemd/user/shared.service"}, {ID: "h", Type: "file", Target: filepath.Join(home, ".config/systemd/user/kept.service"), Kept: "/x"}, {ID: "s1", Type: "service", Target: "watcher.service", Scope: "user", User: "ops"}, {ID: "s2", Type: "service", Target: "shared.service", Scope: "user", User: "ops"}, {ID: "s3", Type: "service", Target: "kept.service", Scope: "user", User: "ops"}, {ID: "s4", Type: "service", Target: "watcher.service"}, }} made := meshMadeUnits(known) for key, want := range map[string]bool{ unitKey("user", "ops", "watcher.service"): true, unitKey("user", "ops", "shared.service"): true, unitKey("user", "ops", "kept.service"): false, unitKey("", "", "watcher.service"): false, unitKey("system", "", "shared.service"): false, } { if made[key] != want { t.Errorf("%s: made %v, want %v", key, made[key], want) } } if installedByHand(known, filepath.Join(home, ".config/systemd/user/watcher.service")) { t.Error("a user unit file the mesh wrote reads as installed by hand") } if !installedByHand(known, filepath.Join(home, ".config/systemd/user/other.service")) { t.Error("a user unit file somebody else put there reads as not installed by hand") } if installedByHand(known, "/usr/lib/systemd/user/pipewire.service") { t.Error("a packaged user unit reads as installed by hand") } } // Undeclared together, the account's unit whose file the mesh wrote is stopped and disabled in the // account's manager — whatever was found — and a system unit of the same name is not touched. func TestAMeshMadeUserUnitIsStoppedInItsAccountAndTheMachinesNamesakeIsNot(t *testing.T) { a := newAccount(t, true) home := t.TempDir() was := homeOf homeOf = func(string) (string, error) { return home, nil } t.Cleanup(func() { homeOf = was }) unitFile := filepath.Join(home, ".config/systemd/user/i3-reload-watcher.service") if err := os.MkdirAll(filepath.Dir(unitFile), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(unitFile, []byte("[Service]\n"), 0o644); err != nil { t.Fatal(err) } a.units["i3-reload-watcher.service"] = &fakeUnit{active: "active", enabled: "enabled"} a.system["i3-reload-watcher.service"] = &fakeUnit{active: "active", enabled: "enabled"} known := store.State{Resources: []store.Applied{ {ID: "i3.unit", Type: "file", Target: unitFile, Origin: store.OriginDeclared}, {ID: "i3.watcher", Type: "service", Target: "i3-reload-watcher.service", Scope: "user", User: "ops", Origin: store.OriginDeclared, Found: &store.FoundUnit{State: "running", Boot: "enabled"}}, }} if _, _, err := applyAccount(t, nothingButA(t), known, a); err != nil { t.Fatal(err) } if u := a.units["i3-reload-watcher.service"]; u.active != "inactive" || u.enabled != "disabled" { t.Fatalf("the mesh's own user unit was not stopped and disabled: %+v", u) } if u := a.system["i3-reload-watcher.service"]; u.active != "active" || u.enabled != "enabled" { t.Fatalf("the machine's unit of the same name was touched: %+v %v", u, a.strays) } } // A service moved from the machine's manager into an account's is two units: the machine's is given // back as it was found, through the machine's manager, and the account's is read afresh. func TestAServiceMovedIntoAnAccountGivesTheMachinesUnitBack(t *testing.T) { a := newAccount(t, true) a.system["i3-reload-watcher.service"] = &fakeUnit{active: "active", enabled: "enabled"} known := store.State{Resources: []store.Applied{ {ID: "i3.watcher", Type: "service", Target: "i3-reload-watcher.service", Origin: store.OriginDeclared, Found: &store.FoundUnit{Unit: "i3-reload-watcher.service", State: "stopped", Boot: "disabled"}}, }} _, known, err := applyAccount(t, declaring(watcher), known, a) if err != nil { t.Fatal(err) } if u := a.system["i3-reload-watcher.service"]; u.active != "inactive" || u.enabled != "disabled" { t.Fatalf("the machine's unit was not given back as found: %+v", u) } if u := a.units["i3-reload-watcher.service"]; u.active != "active" { t.Fatalf("the account's unit was not started: %+v", u) } r, _ := known.Find("i3.watcher") if r.Found == nil || r.Found.State != "stopped" || r.Scope != "user" { t.Fatalf("what was found is not the account's unit's: %+v", r) } } // Lingering is the account's (novox/hq ADR 0177): declared, set and read back; recorded with what // was found; given back on removal while the account still has what the mesh set. func TestLingeringIsDeclaredOnTheAccountAndGivenBack(t *testing.T) { a := newAccount(t, false) user := `{"id":"ops.login","type":"user","name":"ops","linger":true}` report, known, err := applyAccount(t, declaring(user), store.State{}, a) if err != nil { t.Fatal(err) } if !a.did("loginctl enable-linger ops") || outcomeFor(report, "ops.login").Action != "updated" { t.Fatalf("lingering was not enabled: %v", a.asked) } r, _ := known.Find("ops.login") if r.Linger == nil || r.Linger.Found || !r.Linger.Set { t.Fatalf("the record does not say what was found and set: %+v", r.Linger) } a.asked = nil report, known, err = applyAccount(t, declaring(user), known, a) if err != nil { t.Fatal(err) } if a.did("loginctl") || outcomeFor(report, "ops.login").Action != "unchanged" { t.Fatalf("a second apply changed lingering: %v", a.asked) } // And a user-scoped unit of the account now applies with nobody logged in. if _, known, err = applyAccount(t, declaring(user, watcher), known, a); err != nil { t.Fatal(err) } if a.units["i3-reload-watcher.service"].active != "active" { t.Fatal("a lingering account's unit was not started") } report, _, err = applyAccount(t, nothingButA(t), known, a) if err != nil { t.Fatal(err) } if !a.did("loginctl disable-linger ops") { t.Fatalf("lingering was not given back: %v", a.asked) } if o := outcomeFor(report, "ops.login"); o.Action != "restored" || !strings.Contains(o.Detail, "lingering off") { t.Fatalf("%+v", o) } } func TestLingeringTheOperatorChangedSinceIsLeft(t *testing.T) { a := newAccount(t, false) known := store.State{Resources: []store.Applied{{ID: "ops.login", Type: "user", Target: "ops", Origin: store.OriginDeclared, Linger: &store.Lingering{Found: false, Set: true}}}} // Not lingering now: somebody ran disable-linger since the mesh set it. report, _, err := applyAccount(t, nothingButA(t), known, a) if err != nil { t.Fatal(err) } if a.did("loginctl") { t.Fatalf("lingering the operator changed was changed back: %v", a.asked) } if o := outcomeFor(report, "ops.login"); !strings.Contains(o.Detail, "changed since") { t.Fatalf("%+v", o) } } func TestLingeringIsRefusedWhereTheServiceManagerHasNoAccounts(t *testing.T) { alpine, err := system.For("alpine") if err != nil { t.Fatal(err) } a := newAccount(t, false) _, _, err = Apply(context.Background(), alpine, parse(t, declaring( `{"id":"ops.login","type":"user","name":"ops","linger":true}`)), store.State{}, store.OriginDeclared, a.run, nil, nil) if err == nil || !strings.Contains(err.Error(), "linger") { t.Fatalf("not refused: %v", err) } }