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.
92 lines
3.6 KiB
Go
92 lines
3.6 KiB
Go
package broker
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/envfile"
|
|
)
|
|
|
|
// Whether this mesh's own traffic is on the bus being built.
|
|
//
|
|
// **One switch, read in one place** (novox/hq ADR 0116 step 5). Every seam the bus change went
|
|
// behind ships both implementations, and until the rollout every one of them chooses the bus the
|
|
// mesh runs on today. This is what the rollout flips, and it is deliberately a single fact rather
|
|
// than a fact per component: a controller whose outbound is on one bus and whose inbound is on the
|
|
// other is a mesh that hears nothing, and no test of either half would catch it.
|
|
|
|
// NATSVar is where the controller finds the bus being built. Unset is the ordinary case and means
|
|
// the mesh runs on the bus it has always run on.
|
|
const NATSVar = "MESH_BUS_NATS"
|
|
|
|
// OnNATS is the address of the bus being built, and whether the mesh is on it.
|
|
//
|
|
// Read from the node's own settings rather than baked in, for the reason the broker's address is
|
|
// (novox/hq 04-ISSUES/102): an address recorded once does not follow a node's ports.
|
|
func OnNATS() (address string, on bool, err error) {
|
|
address, err = envfile.Placed(NATSVar)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
address = strings.TrimSpace(address)
|
|
if address == "" {
|
|
return "", false, nil
|
|
}
|
|
return address, true, nil
|
|
}
|
|
|
|
// CredentialIn reads the user and password out of a bus address, and the address without them.
|
|
//
|
|
// The controller's own credential arrives in its address, the way the old bus's does. Split out so the
|
|
// controller can record a hash of what it is actually using: its user is created by the installer at a
|
|
// bootstrap password, before the controller exists to mint one, and a composition that left itself out
|
|
// would produce a bus the writer cannot connect to.
|
|
func CredentialIn(address string) (user, password, bare string) {
|
|
at := strings.LastIndex(address, "@")
|
|
if at < 0 {
|
|
return "", "", address
|
|
}
|
|
scheme := ""
|
|
rest := address[:at]
|
|
if i := strings.Index(rest, "://"); i >= 0 {
|
|
scheme, rest = rest[:i+3], rest[i+3:]
|
|
}
|
|
user, password, _ = strings.Cut(rest, ":")
|
|
return user, password, scheme + address[at+1:]
|
|
}
|
|
|
|
// BareAddress is a bus address with any credential stripped, for something that only needs to know
|
|
// whether a server is answering there.
|
|
func BareAddress(address string) string {
|
|
_, _, bare := CredentialIn(address)
|
|
if bare == "" {
|
|
return address
|
|
}
|
|
if strings.Contains(bare, "://") {
|
|
return bare
|
|
}
|
|
return "nats://" + bare
|
|
}
|
|
|
|
// MustBeOneBus refuses a configuration that names both buses for the mesh's own traffic.
|
|
//
|
|
// **Both clients ship and that is the point; both being live is not.** The rollout moves every node
|
|
// at once (ADR 0116 step 5): a mesh half on each is one where a declaration goes out on one bus and
|
|
// the report comes back on the other, and nothing anywhere says so — every component would log
|
|
// success. Refused at start, where it can be said in one sentence.
|
|
func MustBeOneBus(amqp, nats string) error {
|
|
if strings.TrimSpace(amqp) != "" && strings.TrimSpace(nats) != "" {
|
|
return fmt.Errorf(
|
|
"this control plane is told about both buses (%s and %s) and can only be on one. A mesh "+
|
|
"half on each is one where a declaration goes out on one and the report comes back "+
|
|
"on the other, and every component reports success while it happens. The rollout "+
|
|
"moves every node at once: unset %s to stay, or unset %s to move",
|
|
AMQPVarName, NATSVar, NATSVar, AMQPVarName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AMQPVarName is the variable naming the bus the mesh runs on today. Named here rather than
|
|
// imported from the link package, for the one direction of dependency.
|
|
const AMQPVarName = "MESH_BROKER_AMQP"
|