Files
mesh-controller/cmd/mesh-control/board.go
T
jschoubben 0be227bd7e status: one machine that cannot be worked out no longer takes the answer from the rest
`status --json` emitted no JSON at all when a single node was unresolvable. A blocked
node is not on the private network, and a mesh whose hub is that node has no hub — which
came back through the reading as a refusal, so `status` printed nothing and `status
--json` put multi-line prose on stderr and not one byte on stdout. A machine-readable
interface that stops being machine-readable exactly when something is wrong is one nobody
can build an alarm on.

Why a machine cannot be worked out is read as data now, per machine, through the same
whoResolves the private network is built from — so this and the network agree about who
could not be resolved rather than deciding it twice. The private network failing to
compute is kept as a note beside it instead of ending the read: it is almost always a
consequence of those same refusals, and every question that does not depend on it is
still answered.

It reaches all three ways of saying it, from the one reading: the text form leads with it
because a machine here is in none of the answers below, the JSON carries `unresolved`
(always a list, never null) and `network`, and the page has a section of its own.

That also closes a silent success. A machine that resolves to nothing has nothing
computed for it, so there is nothing to compare it against and nothing it can be behind —
it appeared in no answer at all, and `status` reported a mesh where nothing could be sent
anywhere as "all doing what they were told".

statusAsJSON takes the whole reading now rather than a growing argument list, which is
what let an answer be added to the text form and forgotten here. The two are one
function's output in two shapes and must not be able to differ about what was asked.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-10 21:12:06 +02:00

318 lines
11 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"html/template"
"net/http"
"sort"
"strings"
"time"
)
// boardCommand serves the three questions as a page.
//
// **It reads through the same functions everything else does and holds nothing**
// (novox/hq 03-DESIGN/01-to-be/11-a-board.md). The board being replaced is one service that reads
// every context's database directly — [ADR 0008](novox/hq) violated by the one component with a
// reason to violate it, and the cost is that a boundary nothing may cross can move, while one
// thing crossing it is enough to freeze it. A board that reads the provisioning tables is a board
// that breaks when provisioning changes its tables, and the change then gets weighed against the
// board.
//
// **It stores nothing of its own.** No cache that can disagree, no table of what the mesh looked
// like last time. Every request reads the mesh now; if that is slow, the answer belongs in the
// context that owns it, where everything else asking gets it too.
//
// **Reading is the whole of it.** Every action a board could offer already exists as a command,
// and a button that does something no command does is a second implementation of a decision.
func boardCommand(ctx context.Context, args []string) error {
set := flag.NewFlagSet("board", flag.ContinueOnError)
// The private network, not everything. A board says which machines are broken and what they
// are running, which is exactly the map somebody attacking this would like — and there is no
// reason for it to be reachable from further away than the mesh.
listen := set.String("listen", "127.0.0.1:8080", "where to serve it")
if _, err := parseAround(set, args); err != nil {
return err
}
server := &http.Server{
Addr: *listen,
ReadHeaderTimeout: 10 * time.Second,
Handler: board(),
}
fmt.Printf("the board is on http://%s\n", *listen)
fmt.Printf(" it reads the mesh on every request and keeps nothing\n")
go func() {
<-ctx.Done()
closing, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(closing)
}()
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
// board is the handler, separate so a test can drive it without a listener.
func board() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
asked, err := ask(r.Context())
if err != nil {
// **Said, not blank.** A board that cannot reach the mesh and renders an empty page
// says "nothing is wrong" in the one situation where nobody can know that.
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusServiceUnavailable)
_ = page.Execute(w, view{Unreachable: err.Error()})
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := page.Execute(w, asked); err != nil {
// The page is half-written by now; there is nothing useful left to say to the
// browser, and saying it here is what stops the failure being silent.
fmt.Printf("the board could not render: %v\n", err)
}
})
// The same answers for something that is not a person, from the same read. A board and a
// script disagreeing about which machine is broken would be worse than either alone.
mux.HandleFunc("/mesh.json", func(w http.ResponseWriter, r *http.Request) {
open, err := openStores(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer open.Close()
asked, err := theThreeQuestions(r.Context(), open)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
body, err := statusAsJSON(asked)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(append(body, '\n'))
})
return mux
}
// ask reads the mesh for one request.
func ask(ctx context.Context) (view, error) {
open, err := openStores(ctx)
if err != nil {
return view{}, err
}
defer open.Close()
asked, err := theThreeQuestions(ctx, open)
if err != nil {
return view{}, err
}
return viewOf(asked), nil
}
// view is what the page is given. Nothing is derived here that the reader could not derive.
type view struct {
Unreachable string
Machines int
Broken []brokenMachine
Quiet []quietMachine
Behind []staleModule
Waiting []waitingMachine
// Unresolved is every machine that cannot be worked out at all. Shown above everything else,
// because a machine here is in none of the other lists: nothing was computed for it, so it is
// not broken, not quiet and not behind — and a page without this said "all well" about a mesh
// where nothing could be sent anywhere.
Unresolved []blockedMachine
// Network is why the private network could not be computed, when it could not.
Network string
At string
}
type blockedMachine struct {
Node string
// Said is the mesh's own words, a line at a time. Every unmet requirement, not the first:
// a machine is usually blocked by more than one and fixing one of them changes nothing.
Said []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
// the machine is exactly as it was and what is wrong is in what was sent; failed means it is
// in a state nobody declared and what is wrong is on the machine.** They are fixed in
// different places, so one word for both would send half the readers to the wrong one.
Outcome string
Said []string
When string
}
type quietMachine struct {
Node string
// Heard is "never" or how long ago. Never heard from is not the same as quiet for a while:
// one may be a machine that was never sent anything.
Heard string
}
type staleModule struct {
Module string
Holds string
Source string
Running []string
}
func viewOf(asked answers) view {
out := view{Machines: len(asked.nodes), At: time.Now().Format("15:04:05"),
Network: asked.network}
var blocked []string
for name := range asked.refused {
blocked = append(blocked, name)
}
sort.Strings(blocked)
for _, name := range blocked {
one := blockedMachine{Node: name}
for _, line := range strings.Split(strings.TrimRight(asked.refused[name], "\n"), "\n") {
one.Said = append(one.Said, strings.TrimSpace(line))
}
out.Unresolved = append(out.Unresolved, one)
}
for _, d := range asked.wrong {
one := brokenMachine{Node: d.Node, Outcome: d.Outcome,
When: d.At.Local().Format("2006-01-02 15:04")}
if d.Refused != "" {
// The host's own words. It says exactly what it could not accept, and nothing
// written here would say it better.
one.Said = append(one.Said, firstLine(d.Refused))
}
for _, f := range d.Failed {
one.Said = append(one.Said, f.ID+": "+firstLine(f.Error))
}
out.Broken = append(out.Broken, one)
}
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{
Module: module, Holds: short(from.BuiltFrom), Source: short(from.Head), Running: on,
})
}
return out
}
// The page. Deliberately one file with no assets: a board that cannot render without fetching
// something is a board that is blank exactly when the mesh is unwell.
var page = template.Must(template.New("board").Parse(`<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>the mesh</title>
<style>
:root { color-scheme: light dark; }
body { font: 15px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; margin: 2rem auto;
max-width: 52rem; padding: 0 1rem; }
h1 { font-size: 1.1rem; font-weight: 600; margin: 0 0 1.5rem; }
h2 { font-size: 1rem; font-weight: 600; margin: 2rem 0 .5rem; }
.quiet { opacity: .65; }
.said { opacity: .8; padding-left: 1.5rem; }
.outcome { display: inline-block; min-width: 4.5rem; }
.refused { color: #b26b00; }
.failed { color: #c0392b; }
ul { list-style: none; padding: 0; margin: 0; }
li { padding: .15rem 0; }
footer { margin-top: 3rem; opacity: .6; font-size: .85rem; }
</style>
{{if .Unreachable}}
<h1>the mesh cannot be read</h1>
<p class="failed">{{.Unreachable}}</p>
<p class="quiet">This says nothing about whether the mesh is well — only that this page could not
find out.</p>
{{else}}
<h1>{{.Machines}} machine{{if ne .Machines 1}}s{{end}}</h1>
{{if .Unresolved}}
<h2>Can everything be worked out?</h2>
<ul>
{{range .Unresolved}}
<li><span class="outcome failed">blocked</span> <strong>{{.Node}}</strong>
{{range .Said}}<div class="said">{{.}}</div>{{end}}
</li>
{{end}}
</ul>
<p class="quiet">Nothing can be sent to a machine here, and it appears in none of the lists below:
nothing was computed for it, so there is nothing it can be behind.</p>
{{end}}
{{if .Network}}
<p class="failed">The private network could not be computed: {{.Network}}</p>
{{end}}
<h2>Is anything broken?</h2>
{{if .Broken}}
<ul>
{{range .Broken}}
<li>
<span class="outcome {{.Outcome}}">{{.Outcome}}</span>
<strong>{{.Node}}</strong> <span class="quiet">{{.When}}</span>
{{range .Said}}<div class="said">{{.}}</div>{{end}}
</li>
{{end}}
</ul>
{{else}}<p class="quiet">No. Every machine is doing what it was told.</p>{{end}}
<h2>Is anything not answering?</h2>
{{if .Quiet}}
<ul>{{range .Quiet}}<li><strong>{{.Node}}</strong> <span class="quiet">{{.Heard}}</span></li>{{end}}</ul>
<p class="quiet">Not heard from is not the same as tried and could not — a machine here may be
new, switched off, or unreachable.</p>
{{else}}<p class="quiet">No. Every machine has been heard from.</p>{{end}}
<h2>Is anything out of date?</h2>
{{if .Behind}}
<ul>
{{range .Behind}}
<li><strong>{{.Module}}</strong> holds {{.Holds}}, source has {{.Source}}
{{if .Running}}<div class="said">running on {{range $i, $n := .Running}}{{if $i}}, {{end}}{{$n}}{{end}}</div>
{{else}}<div class="said quiet">assigned to nothing</div>{{end}}
</li>
{{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>
`))