The rollout moves every node at once, so there is nothing to inspect afterwards and no half to roll back — either the mesh was ready or it was not. That makes the readiness question the valuable half: it costs nothing, it can be asked of a mesh that is serving as many times as you like, and every answer is a thing somebody can go and fix. It reads from records and dials once. Is a bus answering, does a machine hold the seat, has that machine been sent the composed user list, does every machine have a credential for the new bus, does every module that speaks. Each missing thing names its own next step, because "not ready" that cannot be acted on is not an answer — and this is read at the point where the next step is irreversible. **A machine with no credential is the one that must stop it.** It keeps running and cannot come back, and afterwards there is no bus to tell it anything over, so the remedy has to happen first. The message says so. A module that never reaches the bus is not counted as missing a credential. A third of the catalogue never speaks, and listing those would bury the ones that matter. `rollout --confirm` refuses and says why: the move is not being written before its check has been run against a real mesh. And the plan it prints says the old broker stays — it remains an ordinary provider of `amqp` for whatever else uses it, which on this installation is a whole automation layer that has nothing to do with the mesh. This move is not its retirement, and that is why it is survivable: what breaks if it goes wrong is the mesh's ability to change things, not the services its modules serve.
143 lines
5.4 KiB
Go
143 lines
5.4 KiB
Go
package broker
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Whether a mesh could move its bus, and what is missing if not.
|
|
//
|
|
// **Asked before anything moves, and answerable from records alone.** The rollout moves every node at
|
|
// once (novox/hq ADR 0116 step 5), so there is no partial state to inspect afterwards and no half to
|
|
// roll back: either the mesh was ready or it was not. That makes a readiness question the most
|
|
// valuable thing here — it costs nothing, it can be asked of a running mesh any number of times, and
|
|
// every answer is a thing somebody can go and fix.
|
|
//
|
|
// Deliberately pure. It is handed what the mesh knows and returns sentences; nothing here connects to
|
|
// anything, so it can be asked on a workstation about a mesh it has never reached.
|
|
|
|
// Readiness is what the mesh knows about its own ability to move.
|
|
type Readiness struct {
|
|
// TheBus is the address the mesh's own traffic would move to, empty when nothing names one.
|
|
TheBus string
|
|
// ServerStanding is whether a bus is reachable at that address, as somebody checked.
|
|
ServerStanding bool
|
|
// Holder is the node running the module that holds the bus seat, empty when nothing does.
|
|
Holder string
|
|
// AccountsComposed is whether that node has been sent the composed user list.
|
|
AccountsComposed bool
|
|
// Nodes is every machine the mesh knows.
|
|
Nodes []string
|
|
// Credentialled is which of them has a credential for the new bus.
|
|
Credentialled map[string]bool
|
|
// Modules is every assigned module, as `<node>/<module>`.
|
|
Modules []string
|
|
// ModuleCredentialled is which of those has one.
|
|
ModuleCredentialled map[string]bool
|
|
// StillOnTheOldBus is whether anything of the mesh's own still needs the bus it is leaving —
|
|
// which is not a reason to stop, because that broker stays as an ordinary provider of `amqp`
|
|
// (ADR 0119). Recorded so nobody reads the move as a retirement.
|
|
OldBusHasOtherClients bool
|
|
}
|
|
|
|
// NotReady is every reason this mesh cannot move its bus yet, in the order somebody would fix them.
|
|
//
|
|
// Empty means ready. **Each entry names one thing and what to do about it**, because a readiness check
|
|
// that says "not ready" is a check nobody can act on — and this is read at the point where the next
|
|
// step is irreversible.
|
|
func NotReady(r Readiness) []string {
|
|
var why []string
|
|
|
|
if strings.TrimSpace(r.TheBus) == "" {
|
|
why = append(why, "nothing names the bus to move to: set "+NATSVar+" on the control node "+
|
|
"to the address the new server answers on")
|
|
}
|
|
if !r.ServerStanding {
|
|
why = append(why, "no bus is answering at that address. Step 2 of the change raises it beside "+
|
|
"the one the mesh is on, carrying nothing — assign the module that holds "+
|
|
"mesh-broker and push the machine that runs it")
|
|
}
|
|
if r.Holder == "" {
|
|
why = append(why, "no machine holds mesh-broker, so nothing would compose the bus's user "+
|
|
"list. Assign the module that claims it")
|
|
} else if !r.AccountsComposed {
|
|
why = append(why, fmt.Sprintf(
|
|
"%s holds mesh-broker and has not been sent the composed user list, so the bus would "+
|
|
"refuse every connection. `push %s`", r.Holder, r.Holder))
|
|
}
|
|
|
|
// A node with no credential cannot come back after the move, and a node that cannot come back is
|
|
// a machine the mesh has lost until somebody goes to it.
|
|
var missing []string
|
|
for _, n := range r.Nodes {
|
|
if !r.Credentialled[n] {
|
|
missing = append(missing, n)
|
|
}
|
|
}
|
|
sort.Strings(missing)
|
|
if len(missing) > 0 {
|
|
why = append(why, fmt.Sprintf(
|
|
"%d machine(s) have no credential for the new bus and would not come back: %s. Each needs "+
|
|
"one minted before the move, not after — after, there is no bus to ask over",
|
|
len(missing), strings.Join(missing, ", ")))
|
|
}
|
|
|
|
// A module without one keeps running and stops being reachable, which is a smaller fault and still
|
|
// one somebody should choose rather than discover.
|
|
var quiet []string
|
|
for _, m := range r.Modules {
|
|
if !r.ModuleCredentialled[m] {
|
|
quiet = append(quiet, m)
|
|
}
|
|
}
|
|
sort.Strings(quiet)
|
|
if len(quiet) > 0 {
|
|
why = append(why, fmt.Sprintf(
|
|
"%d module(s) have no credential for the new bus: %s. Each keeps serving and stops "+
|
|
"answering tools and hearing events until it is issued one",
|
|
len(quiet), strings.Join(quiet, ", ")))
|
|
}
|
|
|
|
return why
|
|
}
|
|
|
|
// WhatMoves is what the rollout would do, in order, for somebody reading before they commit.
|
|
//
|
|
// **Written out rather than summarised.** This is the one step with nothing to inspect afterwards, so
|
|
// the last useful moment to disagree with it is while reading this.
|
|
func WhatMoves(r Readiness) []string {
|
|
out := []string{
|
|
fmt.Sprintf("compose the bus's user list and send it to %s", holderOr(r.Holder)),
|
|
fmt.Sprintf("move this control plane to %s, and confirm it is heard", busOr(r.TheBus)),
|
|
}
|
|
nodes := append([]string(nil), r.Nodes...)
|
|
sort.Strings(nodes)
|
|
for _, n := range nodes {
|
|
out = append(out, fmt.Sprintf("move %s, and confirm it reports", n))
|
|
}
|
|
if len(r.Modules) > 0 {
|
|
out = append(out, fmt.Sprintf("move %d module runtime(s), and confirm each answers",
|
|
len(r.Modules)))
|
|
}
|
|
if r.OldBusHasOtherClients {
|
|
out = append(out, "leave the old broker running: it stays an ordinary provider of `amqp` for "+
|
|
"whatever else uses it (ADR 0119), and this move is not its retirement")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func holderOr(node string) string {
|
|
if node == "" {
|
|
return "whichever machine holds mesh-broker"
|
|
}
|
|
return node
|
|
}
|
|
|
|
func busOr(address string) string {
|
|
if address == "" {
|
|
return "the new bus"
|
|
}
|
|
return address
|
|
}
|