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 // Adopted is every node still adopted (novox/hq ADR 0100). Not broken: nothing forces the // flip, so a node left adopted is shown rather than read as converged. Adopted []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, Adopted: adoptedNodes(asked.nodes)} 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(` the mesh {{if .Unreachable}}

the mesh cannot be read

{{.Unreachable}}

This says nothing about whether the mesh is well — only that this page could not find out.

{{else}}

{{.Machines}} machine{{if ne .Machines 1}}s{{end}}

{{if .Unresolved}}

Can everything be worked out?

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.

{{end}} {{if .Network}}

The private network could not be computed: {{.Network}}

{{end}}

Is anything broken?

{{if .Broken}} {{else}}

No. Every machine is doing what it was told.

{{end}}

Is anything not answering?

{{if .Quiet}}

Not heard from is not the same as tried and could not — a machine here may be new, switched off, or unreachable.

{{else}}

No. Every machine has been heard from.

{{end}}

Is anything out of date?

{{if .Behind}} {{else}}

No. Every module is what its source last had.

{{end}} {{if .Waiting}}

Never told is not out of date: nobody has asked that machine to be anything yet. Both are sent by push --behind.

{{else}}

Every machine is running what the mesh would send it.

{{end}} {{if .Adopted}}

Which machines are adopted?

{{end}} {{end}} `))