Say each account's root verdict and the engine's contract in the node's health report, so the self-check's DA and D-root judge without the controller (issue 1952, ADR 0266)
Every account resource marked root never goes into NodeHealth.Root; the statement's contract into Contract. Relogin, root and found are judged each on its own, so an account may be both. Pins mesh-sdk f3677eb. Test: the report's row in TestAHealthStatementIsSaidOnTheLinkOpenNow carries the agent account's verdict and the contract.
This commit is contained in:
@@ -59,7 +59,7 @@ func TestAHealthStatementIsSaidOnTheLinkOpenNow(t *testing.T) {
|
||||
t.Run("the node's health report is written on a change only", func(t *testing.T) {
|
||||
since := time.Date(2026, 10, 29, 20, 0, 0, 0, time.UTC)
|
||||
look := func(streak int, writer string) Health {
|
||||
return Health{At: time.Now(), Network: &NetworkHealth{State: StateUnhealthy, Since: since, Parts: []NetworkPart{
|
||||
return Health{Contract: 7, At: time.Now(), Network: &NetworkHealth{State: StateUnhealthy, Since: since, Parts: []NetworkPart{
|
||||
{Part: "resolv-conf", State: StateUnhealthy, Writer: writer, Owner: "dnsmasq", Since: since, Streak: streak}}},
|
||||
Units: &UnitsHealth{State: "degraded", Failed: []FailedUnit{{Unit: "cups.service", Scope: "system", Since: since}}},
|
||||
Resources: []ResourceHealth{
|
||||
@@ -67,13 +67,16 @@ func TestAHealthStatementIsSaidOnTheLinkOpenNow(t *testing.T) {
|
||||
Reason: "relogin needed: the session began before scanner", Since: since},
|
||||
{Module: "photos", Resource: "photos.library", Kind: KindDirectory, Target: "/srv/photos", State: StateUnhealthy,
|
||||
Reason: ReasonUsedAsFound + " owned by root", Since: since},
|
||||
{Module: "letta", Resource: "letta.server", Kind: "container", State: StateHealthy, Since: since}}}
|
||||
{Module: "letta", Resource: "letta.server", Kind: "container", State: StateHealthy, Since: since},
|
||||
{Module: "claude-code", Resource: "claude-code.agent", Kind: KindAccount, Account: "agent", Root: "never",
|
||||
State: StateHealthy, Since: since}}}
|
||||
}
|
||||
peers := []PeerSeen{{Key: "B=", Seen: true}, {Key: "A=", Addresses: []string{"10.10.0.2/32"}}}
|
||||
r := NodeHealthOf("anchor", look(1, "NetworkManager"), peers, Machines{})
|
||||
if r.Network.Parts[0].Writer != "NetworkManager" || r.Network.Parts[0].Streak != 0 || r.Peers[0].Key != "A=" ||
|
||||
len(r.Units.Failed) != 1 || len(r.Relogin) != 1 || r.Relogin[0].Account != "jochen" ||
|
||||
len(r.Found) != 1 || r.Found[0].Path != "/srv/photos" {
|
||||
len(r.Found) != 1 || r.Found[0].Path != "/srv/photos" ||
|
||||
len(r.Root) != 1 || r.Root[0].Account != "agent" || r.Root[0].State != StateHealthy || r.Contract != 7 {
|
||||
t.Fatalf("the report is %+v", r)
|
||||
}
|
||||
// The machines an unhealthy part points at, as the controller holds it there; none for the machine's own.
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
@@ -81,7 +82,7 @@ const TowardHub = "hub"
|
||||
// streaks, each unhealthy part naming the machines it points at (machines), its machine units, and each account and
|
||||
// directory of its resources that the six alerts read.
|
||||
func NodeHealthOf(node string, h Health, peers []PeerSeen, machines Machines) NodeHealth {
|
||||
out := NodeHealth{V: NodeHealthVersion, Node: node, Units: h.Units}
|
||||
out := NodeHealth{V: NodeHealthVersion, Node: node, Units: h.Units, Contract: h.Contract}
|
||||
if len(peers) > 0 {
|
||||
out.Peers = append([]PeerSeen(nil), peers...)
|
||||
sort.Slice(out.Peers, func(i, k int) bool { return out.Peers[i].Key < out.Peers[k].Key })
|
||||
@@ -99,11 +100,17 @@ func NodeHealthOf(node string, h Health, peers []PeerSeen, machines Machines) No
|
||||
out.Network = &n
|
||||
}
|
||||
for _, r := range h.Resources {
|
||||
switch {
|
||||
case r.Kind == KindAccount && r.State == StateUnhealthy && strings.HasPrefix(r.Reason, ReasonReloginNeeded):
|
||||
// Each alert reads its own: an account may need a new login and be declared never to become root at once.
|
||||
if r.Kind == KindAccount && r.State == StateUnhealthy && strings.HasPrefix(r.Reason, ReasonReloginNeeded) {
|
||||
out.Relogin = append(out.Relogin, NodeAccount{Account: r.Account, Module: r.Module, Resource: r.Resource,
|
||||
Reason: r.Reason, Since: r.Since})
|
||||
case r.Kind == KindDirectory && strings.HasPrefix(r.Reason, ReasonUsedAsFound):
|
||||
}
|
||||
if r.Kind == KindAccount && r.Root == declaration.RootNever {
|
||||
// The account declared never to become root without a person, judged (ADR 0266): what DA and D-root read.
|
||||
out.Root = append(out.Root, delivery.RootVerdict{Account: r.Account, Module: r.Module, Resource: r.Resource,
|
||||
State: r.State, Reason: r.Reason, Since: r.Since})
|
||||
}
|
||||
if r.Kind == KindDirectory && strings.HasPrefix(r.Reason, ReasonUsedAsFound) {
|
||||
out.Found = append(out.Found, NodeDirectory{Module: r.Module, Resource: r.Resource, Path: r.Target,
|
||||
Reason: r.Reason, Since: r.Since})
|
||||
}
|
||||
@@ -112,6 +119,9 @@ func NodeHealthOf(node string, h Health, peers []PeerSeen, machines Machines) No
|
||||
return out.Relogin[i].Account+out.Relogin[i].Module < out.Relogin[k].Account+out.Relogin[k].Module
|
||||
})
|
||||
sort.Slice(out.Found, func(i, k int) bool { return out.Found[i].Path < out.Found[k].Path })
|
||||
sort.Slice(out.Root, func(i, k int) bool {
|
||||
return out.Root[i].Module+out.Root[i].Resource < out.Root[k].Module+out.Root[k].Resource
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user