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 `/`. 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 }