Files
mesh-controller/cmd/mesh-control/board.go
T
jschoubben c0107b8572 Status says when each machine last reported, beside when it was sent
"Not waiting" says the declaration is current, not that the machine
finished applying it: the sent digest is recorded at send. So a test
that pushed, saw waiting clear, and asked the machine what it was
running found containers that did not exist yet — the certificate fix
made compositions stable, and the settling that used to fail first had
been hiding the gap behind it.

The mesh already held the missing half: every machine's last report,
with its time. It just was not in the JSON. `reported` now sets each
machine's last word beside when the current declaration went to it, and
"has it caught up" becomes a comparison of two timestamps the mesh
recorded itself — a report newer than the send means the machine acted
on what was sent; older means it is still working, which waiting alone
cannot distinguish.
2026-09-01 23:02:26 +02:00

274 lines
9.5 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"html/template"
"net/http"
"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.wrong, asked.nodes, asked.quiet, asked.behind, asked.sources,
asked.waiting, asked.reported)
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
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
// 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")}
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>
<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>
`))