diff --git a/go.mod b/go.mod index e49e5bd..a7c393c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029215315-58a83249c80b + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029215805-94d0de5c19b6 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index e33c19d..903dac8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029215315-58a83249c80b h1:1b3bqhFHWosEhxlsjMI6NEWFH+vdD3hqXVIysyLhPYU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029215315-58a83249c80b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029215805-94d0de5c19b6 h1:eQIsSB3Z72MNjirW3TbKbvFgkhwjCAgOAqWd0U4ldCE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029215805-94d0de5c19b6/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/internal/link/health_test.go b/internal/link/health_test.go index 58282a2..88ace64 100644 --- a/internal/link/health_test.go +++ b/internal/link/health_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" ) // A health statement is said on the link open now, as the machine's own word on its own subject, and @@ -95,10 +97,14 @@ func TestAHealthStatementIsSaidOnTheLinkOpenNow(t *testing.T) { t.Fatalf("step %d: wrote %v (want %v), %v", i, wrote, step.wrote, err) } } - got, err := DecodeNodeHealth(store.values["anchor"]) + got, err := delivery.DecodeNodeHealth(store.values["anchor"]) if err != nil || got.Node != "anchor" || got.Network.Parts[0].Writer != "openvpn" || !got.Peers[0].Seen || got.At.IsZero() { t.Fatalf("the key holds %+v, %v", got, err) } + // The key's bytes are mesh-sdk's layout: decoded and encoded again by it, the same bytes. + if again, err := delivery.EncodeNodeHealth(got); err != nil || string(again) != string(store.values["anchor"]) { + t.Fatalf("the key's bytes are not the sdk's layout: %s\n%s (%v)", store.values["anchor"], again, err) + } if store.puts != 3 { t.Fatalf("%d writes, want 3: one per change", store.puts) } diff --git a/internal/link/messages.go b/internal/link/messages.go index 2582562..23376d4 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -313,58 +313,18 @@ const KindRunner = "runner" // ReasonUsedAsFound starts the reason of a directory used as found. const ReasonUsedAsFound = "used as found:" -// UnitsHealth is the machine's service managers in one statement (issue 315). -type UnitsHealth struct { - // State is running, degraded — a unit failed, the modules' or the machine's — or unknown when no - // manager could be read. - State string `json:"state"` - // Failed is every unit failed on two looks in a row that no module places: the machine's own. - Failed []FailedUnit `json:"failed"` - // Unread names a manager that could not be read, with why. - Unread []string `json:"unread,omitempty"` - // Away names an account's own manager not running, so not read: nothing in it is failed (issue 764). - Away []string `json:"away,omitempty"` -} -// FailedUnit is one failed unit no module places. -type FailedUnit struct { - Unit string `json:"unit"` - // Scope is system, or user: an account's own manager. - Scope string `json:"scope"` - // Load is loaded, not-found — a unit whose file is gone and whose failure its manager still holds… - Load string `json:"load,omitempty"` - // Result is how it failed: exit-code, timeout, start-limit-hit… - Result string `json:"result,omitempty"` - // Resource is the mesh's own resource that names it, when the mesh placed it in its own right. - Resource string `json:"resource,omitempty"` - Since time.Time `json:"since"` -} -// NetworkHealth is the machine's networking in one statement (ADR 0241): the worst of its parts, since -// when, and each part. -type NetworkHealth struct { - State string `json:"state"` - Since time.Time `json:"since"` - Parts []NetworkPart `json:"parts"` -} -// NetworkPart is one part of a machine's networking, as its engine judged it on its second look. -type NetworkPart struct { - // Part is resolv-conf, names, tunnel, bus or route. - Part string `json:"part"` - State string `json:"state"` - // Reason is why it is not healthy, in words with no address, path or domain; Said the detail. - Reason string `json:"reason,omitempty"` - Said string `json:"said,omitempty"` - // Writer is the program that rewrote the resolver file, when it can be named; Owner the module whose - // file it is — the uplink's holder. - Writer string `json:"writer,omitempty"` - Owner string `json:"owner,omitempty"` - // Toward is what the failure points at: "hub", or each resolver's address that failed. - Toward []string `json:"toward,omitempty"` - Since time.Time `json:"since"` - Streak int `json:"streak,omitempty"` -} + +// The machine's networking and its service managers in a statement are the node's health report's parts (mesh-sdk +// delivery, issue 774): one layout for the message, the report and the watchman that reads it. +type ( + NetworkHealth = delivery.NetworkHealth + NetworkPart = delivery.NetworkPart + UnitsHealth = delivery.UnitsHealth + FailedUnit = delivery.FailedUnit +) // The states a long-running resource is said in (ADR 0240 §4). const ( diff --git a/internal/link/nodehealth.go b/internal/link/nodehealth.go index 4ec92c9..862a2a0 100644 --- a/internal/link/nodehealth.go +++ b/internal/link/nodehealth.go @@ -1,15 +1,14 @@ package link import ( - "bytes" "context" - "encoding/json" "errors" "fmt" "sort" "strings" "time" + "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" ) @@ -27,58 +26,19 @@ import ( // - relogin-needed: Relogin, each account whose running session lacks a group it was given; // - directory-used-as-found: Found, each directory a module declares that the engine uses as found. // -// The layout moves into mesh-sdk before the push (the lead, 2026-10-29); until then it is this package's. -// HealthBucket is the node-engine's declared state health: key ``, value NodeHealth. -const HealthBucket = "mesh-host_health" +// The report's layout is mesh-sdk's (delivery.NodeHealth): the bucket, its version and its parts. +const ( + HealthBucket = delivery.NodeHealthBucket + NodeHealthVersion = delivery.NodeHealthVersion +) -// NodeHealthVersion is the version of NodeHealth's layout. -const NodeHealthVersion = 1 - -// NodeHealth is one node's health report, as its engine last found it. -type NodeHealth struct { - V int `json:"v"` - Node string `json:"node"` - // Network is the machine's networking, its parts as Health.Network says them, without the streaks; nil where this - // engine does not judge it yet. - Network *NetworkHealth `json:"network,omitempty"` - // Peers are the mesh tunnel's peers on the last look that read them, sorted by key. - Peers []PeerSeen `json:"peers,omitempty"` - // Units is the machine's service managers: each failed unit no module places (Health.Units). - Units *UnitsHealth `json:"units,omitempty"` - // Relogin are the accounts whose running session needs a new login for a group it was given. - Relogin []NodeAccount `json:"relogin,omitempty"` - // Found are the directories used as found. - Found []NodeDirectory `json:"found,omitempty"` - // At is when the engine wrote this, on a change: never a reason to write. - At time.Time `json:"at"` -} - -// PeerSeen is one peer of the mesh's tunnel: its public key, its allowed addresses, and whether this machine saw a -// handshake with it within five minutes. -type PeerSeen struct { - Key string `json:"key"` - Addresses []string `json:"addresses,omitempty"` - Seen bool `json:"seen"` -} - -// NodeAccount is one account marked relogin needed, with the module whose resource it is and why. -type NodeAccount struct { - Account string `json:"account"` - Module string `json:"module,omitempty"` - Resource string `json:"resource,omitempty"` - Reason string `json:"reason"` - Since time.Time `json:"since"` -} - -// NodeDirectory is one directory used as found: the module declaring it, its resource and path, and why. -type NodeDirectory struct { - Module string `json:"module"` - Resource string `json:"resource"` - Path string `json:"path"` - Reason string `json:"reason"` - Since time.Time `json:"since"` -} +type ( + NodeHealth = delivery.NodeHealth + PeerSeen = delivery.PeerSeen + NodeAccount = delivery.NodeAccount + NodeDirectory = delivery.NodeDirectory +) // ReasonReloginNeeded starts an account's reason when its session needs a new login (accounts.ReasonRelogin). const ReasonReloginNeeded = "relogin needed" @@ -116,35 +76,6 @@ func NodeHealthOf(node string, h Health, peers []PeerSeen) NodeHealth { return out } -// EncodeNodeHealth is h as JSON at its version. -func EncodeNodeHealth(h NodeHealth) ([]byte, error) { - h.V = NodeHealthVersion - if h.Node == "" { - return nil, errors.New("a node's health report names its node") - } - return json.Marshal(h) -} - -// DecodeNodeHealth reads a report. -func DecodeNodeHealth(raw []byte) (NodeHealth, error) { - var h NodeHealth - if err := json.Unmarshal(raw, &h); err != nil { - return NodeHealth{}, fmt.Errorf("a node's health report is not readable: %w", err) - } - if h.V != NodeHealthVersion { - return NodeHealth{}, fmt.Errorf("a node's health report of version %d, and this reads %d", h.V, NodeHealthVersion) - } - return h, nil -} - -// SameNodeHealth says two reports say the same, whenever each was written. -func SameNodeHealth(a, b NodeHealth) bool { - a.At, b.At = time.Time{}, time.Time{} - ra, errA := json.Marshal(a) - rb, errB := json.Marshal(b) - return errA == nil && errB == nil && bytes.Equal(ra, rb) -} - // HealthStore is where a node's report is kept: its own key of HealthBucket. type HealthStore interface { // Get is the key's value, nil when it has none. @@ -207,16 +138,16 @@ func writeNodeHealth(ctx context.Context, store HealthStore, last **NodeHealth, return false, fmt.Errorf("the health report of %s cannot be read: %w", h.Node, err) } if raw != nil { - if was, err := DecodeNodeHealth(raw); err == nil { + if was, err := delivery.DecodeNodeHealth(raw); err == nil { *last = &was } } } - if *last != nil && SameNodeHealth(**last, h) { + if *last != nil && delivery.SameNodeHealth(**last, h) { return false, nil } h.At = now().UTC() - raw, err := EncodeNodeHealth(h) + raw, err := delivery.EncodeNodeHealth(h) if err != nil { return false, err }