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.
151 lines
5.7 KiB
Go
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
|
|
}
|