The mesh writes its own user list, and at genesis there is no mesh yet to write it. So the installer carries the first one — the controller's own account at a well-known bootstrap password, exactly as the store is reached at `postgres:bootstrap` and the old bus at `guest:guest`, and rotated with them. From the controller's first composition onward the file is the controller's. That left a gap I would not have found by reading: the controller's own account is created before there is a controller to mint one, so nothing recorded a hash for it, and its first composition would have left the writer out of the file it was writing — a bus nothing can connect to, produced by the thing connected to it. It now records a hash of the credential it is actually using, and only if none is recorded, so a restart cannot put the bootstrap password back over a rotated one. The carried list and the derived one are two statements of one fact, so a test compares them: every subject the controller derives must be in the template, and nothing wider. It earned itself immediately — the composer was granting both a role's whole event branch and the one event it actually follows, which is a wider way of saying the same thing, and the wider one wins. Only the submitting half of a role is granted now; what comes back is named exactly. Getting this wrong is the worst kind of silent. A controller whose carried permissions are narrower than the ones it derives comes up, connects, and is refused on the first thing it tries, with an authorisation error naming a subject and not the template that forgot it — and a mesh cannot be raised twice to find out.
79 lines
3.3 KiB
Go
79 lines
3.3 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:]
|
|
}
|
|
|
|
// 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"
|