diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 4a0dc3f..31aa626 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -1565,7 +1565,7 @@ func healthAsReported(st liveness.Statement, ns *network.Statement) *link.Health for _, r := range st.Resources { h.Resources = append(h.Resources, link.ResourceHealth{Module: r.Module, Resource: r.ID, Kind: r.Kind, Target: r.Target, State: r.State, Reason: r.Reason, Since: r.Since.UTC(), Streak: r.Streak, - Restarts: r.Restarts, Check: r.CheckOf(), Needs: r.NeedsOf()}) + Restarts: r.Restarts, Check: r.CheckOf(), Needs: r.NeedsOf(), Account: userOf(r.Scope, r.User)}) } if ns != nil && ns.State != "" { h.Network = &link.NetworkHealth{State: ns.State, Since: ns.Since.UTC(), Parts: []link.NetworkPart{}} @@ -1600,7 +1600,7 @@ func withUnits(h *link.Health, us *units.Statement) *link.Health { } h.Resources = append(h.Resources, link.ResourceHealth{Module: f.Module, Resource: f.Resource, Kind: link.KindUnit, Target: f.Unit, State: link.StateUnhealthy, Reason: reason, Since: f.Since.UTC(), - Streak: f.Streak}) + Streak: f.Streak, Account: userOf(f.Scope, f.User)}) } return h } @@ -1615,11 +1615,20 @@ func withAccounts(h *link.Health, as *accounts.Statement) *link.Health { for _, v := range as.Accounts { h.Resources = append(h.Resources, link.ResourceHealth{Module: v.Module, Resource: v.ID, Kind: link.KindAccount, Target: v.Name, State: v.State, Reason: v.Reason, Since: v.Since.UTC(), - Streak: v.Streak}) + Streak: v.Streak, Account: v.Name}) } return h } +// userOf is the account whose own service manager a unit is in (novox/hq ADR 0254), empty for the +// machine's own manager. +func userOf(scope, user string) string { + if scope == declaration.ScopeUser { + return user + } + return "" +} + // failedUnitWords is a failed unit as the console says it. func failedUnitWords(f units.Failed) string { whose := "no module places it" diff --git a/cmd/mesh-host/units_test.go b/cmd/mesh-host/units_test.go index e4e6f2b..e5c6654 100644 --- a/cmd/mesh-host/units_test.go +++ b/cmd/mesh-host/units_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/novox/mesh-host/internal/accounts" + "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/link" "github.com/novox/mesh-host/internal/liveness" "github.com/novox/mesh-host/internal/units" @@ -17,7 +19,7 @@ func TestTheStatementCarriesTheFailedUnits(t *testing.T) { at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) us := &units.Statement{At: at, State: units.Degraded, Failed: []units.Failed{ {Unit: "openrazer-daemon.service", Scope: units.ScopeUser, Load: "loaded", Result: "exit-code", - Module: "openrazer", Resource: "openrazer.daemon", Via: units.ViaPackage, Since: at, Streak: 2}, + User: "operator", Module: "openrazer", Resource: "openrazer.daemon", Via: units.ViaPackage, Since: at, Streak: 2}, {Unit: "storage-media.mount", Scope: units.ScopeSystem, Load: "loaded", Result: "timeout", Since: at, Streak: 2}, }} h := withUnits(healthAsReported(liveness.Statement{At: at}, nil), us) @@ -30,6 +32,10 @@ func TestTheStatementCarriesTheFailedUnits(t *testing.T) { !strings.Contains(r.Reason, "account's own service manager") || !strings.Contains(r.Reason, "exit-code") { t.Fatalf("the module's failed unit: %+v", r) } + // Whose manager it fails in, so the controller can tell it from a fault (novox/hq ADR 0254). + if r.Account != "operator" { + t.Fatalf("the unit in the account's own manager does not name the account: %q", r.Account) + } if h.Units == nil || h.Units.State != units.Degraded || len(h.Units.Failed) != 1 || h.Units.Failed[0].Unit != "storage-media.mount" || h.Units.Failed[0].Result != "timeout" { t.Fatalf("the machine's own: %+v", h.Units) @@ -41,3 +47,31 @@ func TestTheStatementCarriesTheFailedUnits(t *testing.T) { t.Fatal("a judge not yet given a declaration said units") } } + +// Every resource that runs in an account's own service manager names the account, and so does the account +// a module put in a group (novox/hq ADR 0254): the controller reads a unit that cannot run before a new login +// by the two together. What the machine's own manager runs names none. +func TestTheStatementNamesTheAccountsManager(t *testing.T) { + at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) + st := liveness.Statement{At: at, Resources: []liveness.State{ + {Resource: liveness.Resource{Module: "openrazer", ID: "openrazer.daemon", Kind: "service", + Target: "openrazer-daemon.service", Scope: declaration.ScopeUser, User: "operator"}, State: liveness.Unhealthy, + Reason: "down", Since: at}, + {Resource: liveness.Resource{Module: "docker", ID: "docker.engine", Kind: "service", Target: "docker.service", + Scope: declaration.ScopeSystem}, State: liveness.Healthy, Since: at}, + }} + as := &accounts.Statement{At: at, Accounts: []accounts.Verdict{{Account: accounts.Account{Module: "openrazer", + ID: "openrazer.operator", Name: "operator", Groups: []string{"openrazer"}}, State: accounts.Unhealthy, + Reason: accounts.ReasonRelogin + ": operator is in the group openrazer", Since: at}}} + h := withAccounts(healthAsReported(st, nil), as) + got := map[string]string{} + for _, r := range h.Resources { + got[r.Resource] = r.Account + } + want := map[string]string{"openrazer.daemon": "operator", "docker.engine": "", "openrazer.operator": "operator"} + for id, account := range want { + if a, said := got[id]; !said || a != account { + t.Errorf("%s names the account %q (said %v); want %q", id, a, said, account) + } + } +} diff --git a/internal/link/messages.go b/internal/link/messages.go index 0931fad..54d2e3a 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -350,6 +350,11 @@ type ResourceHealth struct { // provision it exercises, for the controller to hold what it finds under the provider (to-be 48 ยง6). Check string `json:"check,omitempty"` Needs string `json:"needs,omitempty"` + // Account is the account whose own service manager runs it, for a unit or a service in an account's + // manager, and the account itself for a resource of kind KindAccount (novox/hq ADR 0254): what lets the + // controller tell a unit that cannot run before a new login from one that is broken. Empty for anything + // the machine's own manager or a container runtime runs. + Account string `json:"account,omitempty"` } // HealthSaid is the health event: a machine's statement between its reports, on HealthSubject.