A workstation's per-user daemons — a window manager's reload watcher, an
audio mask, a memory guard — are units in the operator account's own service
manager, and until now had no form the mesh could send (to-be 29). The
`service` shape gains `scope` ("system", the default, or "user") and `user`
(the account, named ${machine:account} by a module); a user-scoped unit
without an account, or a system unit naming one, is refused at parse.
The host reaches the account's manager as `systemctl --user --machine=<account>@`
from its own process: no environment to forge, no user to switch to. Done on
the runner rather than per system, since every system's reading of a unit
already goes through systemctl. Apply, reflect-only and removal all go through
the same manager, and the applied record carries scope and user so removal
gives the unit back to the manager it came from. It answers only while that
manager runs — a login, or lingering enabled for the account; declaring
lingering is a follow-up.
Tests: a user-scoped unit is started and enabled in the account's manager and
recorded with its scope; a system unit never sees --user; the validation of
scope and user.
100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0177: a unit in the operator account's own service manager is applied
|
|
// through that manager — `systemctl --user --machine=<account>@` — and never as a system unit of
|
|
// the same name; its record remembers the scope so removal goes the same way.
|
|
|
|
// accountManager is a user's service manager whose one unit starts when asked, recording the
|
|
// commands and refusing any that reach it outside the account's scope.
|
|
func accountManager(account string, commands *[]string) Runner {
|
|
active, enabled := false, false
|
|
return func(_ context.Context, name string, args ...string) (string, error) {
|
|
line := name + " " + strings.Join(args, " ")
|
|
*commands = append(*commands, line)
|
|
if name == "systemctl" && !strings.HasPrefix(line, "systemctl --user --machine="+account+"@ ") {
|
|
return "", fmt.Errorf("a system-scope command reached the account's manager: %s", line)
|
|
}
|
|
switch {
|
|
case strings.Contains(line, " start "):
|
|
active = true
|
|
return "", nil
|
|
case strings.Contains(line, " stop "):
|
|
active = false
|
|
return "", nil
|
|
case strings.Contains(line, " enable "):
|
|
enabled = true
|
|
return "", nil
|
|
case strings.Contains(line, " disable "):
|
|
enabled = false
|
|
return "", nil
|
|
case strings.Contains(line, "is-enabled"):
|
|
if enabled {
|
|
return "enabled", nil
|
|
}
|
|
return "disabled", 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 TestAUserScopedUnitIsAppliedThroughTheAccountsManager(t *testing.T) {
|
|
var commands []string
|
|
decl := `{"declaration":1,"resources":[
|
|
{"id":"i3.watcher","type":"service","unit":"i3-reload-watcher.service","state":"running","boot":"enabled","scope":"user","user":"ops"}
|
|
]}`
|
|
report, known, err := Apply(context.Background(), archHost(t), parse(t, decl),
|
|
store.State{}, store.OriginDeclared, accountManager("ops", &commands), nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, strings.Join(commands, "\n"))
|
|
}
|
|
if !report.Changed() {
|
|
t.Fatal("a unit that was stopped and is now running changed nothing")
|
|
}
|
|
var started, enabled bool
|
|
for _, c := range commands {
|
|
if c == "systemctl --user --machine=ops@ start i3-reload-watcher.service" {
|
|
started = true
|
|
}
|
|
if c == "systemctl --user --machine=ops@ enable i3-reload-watcher.service" {
|
|
enabled = true
|
|
}
|
|
}
|
|
if !started || !enabled {
|
|
t.Fatalf("the unit was not started and enabled in the account's manager:\n%s", strings.Join(commands, "\n"))
|
|
}
|
|
recorded, ok := known.At("service", "i3-reload-watcher.service")
|
|
if !ok || recorded.Scope != "user" || recorded.User != "ops" {
|
|
t.Fatalf("the record does not say whose manager the unit is in: %+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") {
|
|
t.Fatalf("a system unit was addressed to an account's manager: %s", c)
|
|
}
|
|
}
|
|
}
|