Take the node's health report layout from mesh-sdk 94d0de5, so the engine and the watchman read one form (issue 774)
delivery.NodeHealth and its parts replace this package's copies. The message's network and units parts are aliases of the same types, so its JSON is unchanged. The engine keeps the derivation from its statement and the write on a change. Test: the key's bytes, decoded and encoded again by the sdk, are the same bytes.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+15
-84
@@ -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 `<node>`, 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user