The board says what is waiting too, or it disagrees with the command
Adding "not running what the mesh would send it" to `status` and not to the board would have left two answers to one question with a person in front of each — which is the single thing this page's design forbids, introduced by the change that was supposed to make the question answerable. The published JSON carries it as well, so the page, the command and anything built against either say the same thing from the same read. Additive, because that shape is hard to change once anything is built against it. Never told stays separate from out of date on the page as it is everywhere else: same remedy, and nobody has ever asked that machine to be anything.
This commit is contained in:
@@ -94,7 +94,8 @@ func board() http.Handler {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
body, err := statusAsJSON(asked.wrong, asked.nodes, asked.quiet, asked.behind, asked.sources)
|
||||
body, err := statusAsJSON(asked.wrong, asked.nodes, asked.quiet, asked.behind, asked.sources,
|
||||
asked.waiting)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -127,9 +128,17 @@ type view struct {
|
||||
Broken []brokenMachine
|
||||
Quiet []quietMachine
|
||||
Behind []staleModule
|
||||
Waiting []waitingMachine
|
||||
At string
|
||||
}
|
||||
|
||||
type waitingMachine struct {
|
||||
Node string
|
||||
// Never told is not out of date: nobody has ever asked this machine to be anything. Same
|
||||
// remedy, different situation, and the page says which.
|
||||
Never bool
|
||||
}
|
||||
|
||||
type brokenMachine struct {
|
||||
Node string
|
||||
// Outcome is refused or failed, and stays distinct all the way to the page. **Refused means
|
||||
@@ -173,6 +182,9 @@ func viewOf(asked answers) view {
|
||||
for _, n := range asked.quiet {
|
||||
out.Quiet = append(out.Quiet, quietMachine{Node: n.Name, Heard: heardFrom(n)})
|
||||
}
|
||||
for _, m := range asked.waiting {
|
||||
out.Waiting = append(out.Waiting, waitingMachine{Node: m.Node, Never: m.Never})
|
||||
}
|
||||
for module, on := range asked.behind {
|
||||
from := asked.sources[module]
|
||||
out.Behind = append(out.Behind, staleModule{
|
||||
@@ -242,6 +254,19 @@ new, switched off, or unreachable.</p>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}<p class="quiet">No. Every module is what its source last had.</p>{{end}}
|
||||
|
||||
{{if .Waiting}}
|
||||
<ul>
|
||||
{{range .Waiting}}
|
||||
<li><strong>{{.Node}}</strong>
|
||||
{{if .Never}}<span class="quiet">has never been sent anything</span>
|
||||
{{else}}<span class="quiet">is not running what the mesh would send it</span>{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p class="quiet">Never told is not out of date: nobody has asked that machine to be anything yet.
|
||||
Both are sent by <code>push --behind</code>.</p>
|
||||
{{else}}<p class="quiet">Every machine is running what the mesh would send it.</p>{{end}}
|
||||
{{end}}
|
||||
|
||||
<footer>Read at {{.At}}. This page holds nothing and changes nothing.</footer>
|
||||
|
||||
@@ -119,3 +119,36 @@ func render(t *testing.T, v view) string {
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// The board and the command must answer the same question the same way.
|
||||
//
|
||||
// A machine not running what the mesh would send it was added to `status` and would have been
|
||||
// missing here — which is the one thing this page's design forbids: two answers to the same
|
||||
// question, disagreeing, with a person in front of each.
|
||||
func TestTheBoardSaysWhichMachinesHaveNotBeenSentWhatTheyShouldBe(t *testing.T) {
|
||||
rendered := render(t, viewOf(answers{
|
||||
nodes: []inventory.Node{{Name: "anchor"}, {Name: "laptop"}},
|
||||
waiting: []inventory.Machine{
|
||||
{Node: "anchor"},
|
||||
{Node: "laptop", Never: true},
|
||||
},
|
||||
}))
|
||||
if !strings.Contains(rendered, "is not running what the mesh would send it") {
|
||||
t.Fatalf("the page does not say a machine is out of date:\n%s", rendered)
|
||||
}
|
||||
// Never told is a different situation with the same remedy, and the page keeps them apart.
|
||||
if !strings.Contains(rendered, "has never been sent anything") {
|
||||
t.Fatalf("the page does not separate never told from out of date:\n%s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "push --behind") {
|
||||
t.Fatalf("the page does not say what sends them:\n%s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
// And says so when there is nothing waiting, rather than leaving the question unanswered.
|
||||
func TestAMeshWithNothingWaitingSaysSo(t *testing.T) {
|
||||
rendered := render(t, viewOf(answers{nodes: []inventory.Node{{Name: "anchor"}}}))
|
||||
if !strings.Contains(rendered, "Every machine is running what the mesh would send it") {
|
||||
t.Fatalf("the page leaves the question unanswered:\n%s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1848,7 +1848,7 @@ func statusCommand(ctx context.Context, args []string) error {
|
||||
behind, sources := asked.behind, asked.sources
|
||||
|
||||
if *asJSON {
|
||||
body, err := statusAsJSON(wrong, nodes, quiet, behind, sources)
|
||||
body, err := statusAsJSON(wrong, nodes, quiet, behind, sources, asked.waiting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ type meshStatus struct {
|
||||
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"`
|
||||
// Machines is how many the mesh knows about, so a reader can tell "none wrong" from
|
||||
// "none at all".
|
||||
Machines int `json:"machines"`
|
||||
@@ -50,6 +54,14 @@ type machineDoing struct {
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
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
|
||||
@@ -66,10 +78,16 @@ type moduleBehind struct {
|
||||
|
||||
// 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) ([]byte, error) {
|
||||
behind map[string][]string, sources map[string]inventory.Source,
|
||||
waiting []inventory.Machine) ([]byte, error) {
|
||||
|
||||
out := meshStatus{Machines: len(nodes), Wrong: []machineDoing{},
|
||||
Quiet: []machineQuiet{}, Behind: []moduleBehind{}}
|
||||
Quiet: []machineQuiet{}, Behind: []moduleBehind{}, Waiting: []machineWaiting{}}
|
||||
|
||||
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{
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
func statusOf(t *testing.T, wrong []inventory.Doing, nodes, quiet []inventory.Node,
|
||||
behind map[string][]string, sources map[string]inventory.Source) map[string]any {
|
||||
t.Helper()
|
||||
body, err := statusAsJSON(wrong, nodes, quiet, behind, sources)
|
||||
body, err := statusAsJSON(wrong, nodes, quiet, behind, sources, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func TestNoSecretIsInWhatABoardReads(t *testing.T) {
|
||||
body, err := statusAsJSON(
|
||||
[]inventory.Doing{{Node: "a", Outcome: inventory.OutcomeRefused,
|
||||
Refused: "resource \"x\": a file needs a path"}},
|
||||
[]inventory.Node{{Name: "a"}}, nil, nil, nil)
|
||||
[]inventory.Node{{Name: "a"}}, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user