A unit may be user-scoped: applied through the account's own manager (hq ADR 0177)
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.
This commit is contained in:
@@ -58,6 +58,8 @@ type Outcome struct {
|
||||
kept string
|
||||
// stateless is a service whose unit's lifecycle is the machine's (novox/hq ADR 0117).
|
||||
stateless bool
|
||||
// scope and user are, for a service, whose manager it was applied through (novox/hq ADR 0177).
|
||||
scope, user string
|
||||
// found is, for a service, its unit as the host first found it (novox/hq ADR 0118).
|
||||
found *store.FoundUnit
|
||||
// reads is, for a container, the digest of each file it was created reading, by path — so
|
||||
@@ -563,6 +565,8 @@ func ApplyKeeping(
|
||||
Kept: kept,
|
||||
Reads: outcome.reads,
|
||||
Stateless: outcome.stateless,
|
||||
Scope: outcome.scope,
|
||||
User: outcome.user,
|
||||
Found: outcome.found,
|
||||
Holds: holds(resource),
|
||||
})
|
||||
@@ -1043,10 +1047,12 @@ type unitReloader interface {
|
||||
|
||||
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||
changed map[string]bool, previous store.Applied) (Outcome, error) {
|
||||
run = managerFor(r.Scope, r.User, run)
|
||||
if r.Stateless() {
|
||||
return reflectOnly(ctx, sys, r, run, changed)
|
||||
}
|
||||
out := begin(r)
|
||||
out.scope, out.user = r.Scope, r.User
|
||||
var changes []string
|
||||
|
||||
// A file the service reflects changed, and it may be the unit's own file or a drop-in: the
|
||||
@@ -1186,7 +1192,9 @@ func applyService(ctx context.Context, sys system.System, r *declaration.Service
|
||||
// a machine that uses another — and it reads the change when whatever starts it does.
|
||||
func reflectOnly(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||
changed map[string]bool) (Outcome, error) {
|
||||
run = managerFor(r.Scope, r.User, run)
|
||||
out := begin(r)
|
||||
out.scope, out.user = r.Scope, r.User
|
||||
out.stateless = true
|
||||
restart := reflected(r, changed)
|
||||
reload := restartedBy(r.ReloadOn, changed)
|
||||
@@ -2235,6 +2243,7 @@ func declaredDigest(r declaration.Resource) string {
|
||||
// what was actually done — a unit already as it was found is forgotten, not "restored".
|
||||
func removeService(ctx context.Context, sys system.System, a store.Applied, run Runner,
|
||||
made bool) (string, string, error) {
|
||||
run = managerFor(a.Scope, a.User, run)
|
||||
if a.Stateless {
|
||||
// Declared with no state (novox/hq ADR 0117): its lifecycle was never the mesh's, and the
|
||||
// declaration that said so is the operator's word to hold to, a file of the mesh's or not.
|
||||
@@ -2409,3 +2418,23 @@ func moduleOf(identity string) (string, bool) {
|
||||
}
|
||||
return identity[:at], true
|
||||
}
|
||||
|
||||
// managerFor routes a unit's commands to the manager it belongs to (novox/hq ADR 0177). A system
|
||||
// unit's go to the machine's manager as they always did. A user-scoped unit's go to the account's
|
||||
// own: `systemctl --user --machine=<account>@`, which reaches that manager from the host's own
|
||||
// process without an environment to forge or a user to switch to — and which only answers while
|
||||
// the account's manager runs (a login, or lingering enabled for the account). Done on the runner
|
||||
// rather than in each system: every system's reading of a unit already goes through `systemctl`,
|
||||
// so this is one place instead of one per system and one per method.
|
||||
func managerFor(scope, user string, run Runner) Runner {
|
||||
if scope != declaration.ScopeUser || user == "" {
|
||||
return run
|
||||
}
|
||||
return func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name != "systemctl" {
|
||||
return run(ctx, name, args...)
|
||||
}
|
||||
scoped := append([]string{"--user", "--machine=" + user + "@"}, args...)
|
||||
return run(ctx, name, scoped...)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user