Files
mesh-controller/cmd/mesh-control/readable.go
T
jschoubben 1b63e21c0f Caught up is an equality, not an ordering
The report carries the digest of the declaration it applied (mesh-host
8211d8b), and the mesh stores it beside the outcome. `reported` rows in
the status JSON now say `current`: whether the machine's last word
names the declaration last sent.

Not derivable from the timestamps beside it, which is why they were
not enough: an apply begun under the previous declaration reports
after the next send — newer, and still about the old words. The lab
lost exactly that race between one test's closing push and the next
test's opening one.

Empty digests — every host from before reports carried one — read as
not current, which errs toward waiting rather than toward asserting on
files that are not there yet.
2026-09-02 00:02:44 +02:00

151 lines
5.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"time"
"github.com/novox/mesh-control/internal/inventory"
)
// The same answers, in a shape something other than a person can read.
//
// A board reads through interfaces and holds nothing (novox/hq 03-DESIGN/01-to-be/11-a-board.md).
// Everything it needs is already answered by these commands — as text, for people, which is not
// something a page can read. So each of them can say it again as JSON.
//
// **`--json` rather than a serving API**, because nothing needs one yet: whatever serves a board
// runs the command, and the constraint in the design holds either way — the board never touches a
// context's store. An API is the larger thing and should wait until something is asking for it.
//
// **These shapes are hard to change once anything is built against them.** So they stay close to
// what the domain already calls things, and carry no summary field that would have to be kept
// true. Nothing here is derived that a reader could not derive.
// meshStatus is what `status --json` says: the three questions, in the order they are asked.
type meshStatus struct {
// Wrong is every machine whose last declaration was refused or partly failed.
Wrong []machineDoing `json:"wrong"`
// Quiet is every machine not heard from lately. Not the same as wrong: new, switched off and
// unreachable are not "tried and could not".
Quiet []machineQuiet `json:"quiet"`
// Behind is every module built from something older than its source has.
Behind []moduleBehind `json:"behind"`
// Waiting is every machine not running what the mesh would send it. The same question as
// Behind one level down: that says the catalogue is old, this says a machine is — and only
// this one has somebody's change waiting inside it.
Waiting []machineWaiting `json:"waiting"`
// Reported is every machine's last word beside when it was last sent a declaration. A
// machine whose report is newer than its send has acted on the current declaration; one
// whose is older is still working — and Waiting cannot tell those apart, because the sent
// digest is recorded at send, not at apply.
Reported []machineReported `json:"reported"`
// Machines is how many the mesh knows about, so a reader can tell "none wrong" from
// "none at all".
Machines int `json:"machines"`
}
type machineDoing struct {
Node string `json:"node"`
// Outcome is refused or failed. Kept distinct all the way out: they are fixed in different
// places, and one word for both sends half the readers to the wrong one.
Outcome string `json:"outcome"`
Refused string `json:"refused,omitempty"`
Failed []struct {
ID string `json:"id"`
Error string `json:"error"`
} `json:"failed,omitempty"`
Applied int `json:"applied"`
At time.Time `json:"at"`
}
type machineReported struct {
Node string `json:"node"`
Outcome string `json:"outcome"`
At *time.Time `json:"at,omitempty"`
Sent *time.Time `json:"sent,omitempty"`
// Current is whether the last report names the declaration last sent. Not derivable from
// the timestamps beside it: an apply begun under the previous declaration reports after the
// next send, newer and still about the old words.
Current bool `json:"current"`
}
type machineWaiting struct {
Node string `json:"node"`
// Never is true when nothing has ever been sent to it. Not out of date: nobody has ever asked
// this machine to be anything, and the two read differently to whoever is looking.
Never bool `json:"never"`
Sent *time.Time `json:"sent,omitempty"`
}
type machineQuiet struct {
Node string `json:"node"`
// LastSeen is absent when the machine has never spoken, which is a different thing from
// having been quiet for a while.
LastSeen *time.Time `json:"lastSeen,omitempty"`
}
type moduleBehind struct {
Module string `json:"module"`
BuiltFrom string `json:"builtFrom"`
Head string `json:"head"`
On []string `json:"on"`
}
// statusAsJSON answers the same three questions as the text form, from the same calls.
func statusAsJSON(wrong []inventory.Doing, nodes []inventory.Node, quiet []inventory.Node,
behind map[string][]string, sources map[string]inventory.Source,
waiting []inventory.Machine, reported []inventory.Reported) ([]byte, error) {
out := meshStatus{Machines: len(nodes), Wrong: []machineDoing{},
Quiet: []machineQuiet{}, Behind: []moduleBehind{}, Waiting: []machineWaiting{},
Reported: []machineReported{}}
for _, r := range reported {
out.Reported = append(out.Reported, machineReported{
Node: r.Node, Outcome: r.Outcome, At: r.At, Sent: r.Sent, Current: r.Current})
}
for _, m := range waiting {
out.Waiting = append(out.Waiting, machineWaiting{
Node: m.Node, Never: m.Never, Sent: m.SentAt})
}
for _, d := range wrong {
row := machineDoing{
Node: d.Node, Outcome: d.Outcome, Refused: d.Refused, Applied: d.Applied, At: d.At,
}
for _, f := range d.Failed {
row.Failed = append(row.Failed, struct {
ID string `json:"id"`
Error string `json:"error"`
}{ID: f.ID, Error: f.Error})
}
out.Wrong = append(out.Wrong, row)
}
for _, n := range quiet {
row := machineQuiet{Node: n.Name}
if !n.LastSeen.IsZero() {
seen := n.LastSeen
row.LastSeen = &seen
}
out.Quiet = append(out.Quiet, row)
}
for module, on := range behind {
from := sources[module]
out.Behind = append(out.Behind, moduleBehind{
Module: module, BuiltFrom: from.BuiltFrom, Head: from.Head, On: on,
})
}
return json.MarshalIndent(out, "", " ")
}
// say prints a value as JSON, for the commands that can answer either way.
func say(value any) error {
body, err := json.MarshalIndent(value, "", " ")
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}