Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
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>
|
||||
`))
|
||||
Reference in New Issue
Block a user