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.
98 lines
3.5 KiB
Go
98 lines
3.5 KiB
Go
package broker
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Whether a mesh could move its bus.
|
|
//
|
|
// Every case here is a way of moving that leaves something behind, and the one that matters most is a
|
|
// machine with no credential: after the move there is no bus to ask it over, so it is lost until
|
|
// somebody walks to it.
|
|
|
|
func aMeshReadyToMove() Readiness {
|
|
return Readiness{
|
|
TheBus: "nats://127.0.0.1:5671", ServerStanding: true,
|
|
Holder: "anchor", AccountsComposed: true,
|
|
Nodes: []string{"anchor", "laptop"},
|
|
Credentialled: map[string]bool{"anchor": true, "laptop": true},
|
|
Modules: []string{"anchor/gitea"},
|
|
ModuleCredentialled: map[string]bool{"anchor/gitea": true},
|
|
}
|
|
}
|
|
|
|
func TestAMeshWithEverythingInPlaceIsReady(t *testing.T) {
|
|
if why := NotReady(aMeshReadyToMove()); len(why) != 0 {
|
|
t.Fatalf("a mesh with everything in place was refused: %v", why)
|
|
}
|
|
}
|
|
|
|
// **A machine with no credential is the one that must stop this.** It keeps running and cannot come
|
|
// back, and there is no bus left to tell it anything over — so the remedy has to happen before, and
|
|
// the message says so.
|
|
func TestAMachineWithNoCredentialStopsTheMove(t *testing.T) {
|
|
r := aMeshReadyToMove()
|
|
r.Credentialled = map[string]bool{"anchor": true}
|
|
|
|
why := NotReady(r)
|
|
if len(why) == 0 {
|
|
t.Fatal("a machine that could not come back did not stop the move")
|
|
}
|
|
said := strings.Join(why, "\n")
|
|
if !strings.Contains(said, "laptop") {
|
|
t.Errorf("the refusal does not name the machine: %s", said)
|
|
}
|
|
if !strings.Contains(said, "before the move") {
|
|
t.Errorf("the refusal does not say the remedy comes first: %s", said)
|
|
}
|
|
}
|
|
|
|
// A bus nobody has raised, a seat nobody holds, and a user list nobody has been sent: each stops it,
|
|
// and each names its own next step, because "not ready" that cannot be acted on is not an answer.
|
|
func TestEachThingMissingNamesItsOwnRemedy(t *testing.T) {
|
|
for _, c := range []struct {
|
|
what string
|
|
break_ func(*Readiness)
|
|
says string
|
|
}{
|
|
{"no address", func(r *Readiness) { r.TheBus = "" }, NATSVar},
|
|
{"no server", func(r *Readiness) { r.ServerStanding = false }, "carrying nothing"},
|
|
{"no holder", func(r *Readiness) { r.Holder = "" }, "mesh-broker"},
|
|
{"no user list", func(r *Readiness) { r.AccountsComposed = false }, "push anchor"},
|
|
{"a module with none", func(r *Readiness) {
|
|
r.ModuleCredentialled = map[string]bool{}
|
|
}, "anchor/gitea"},
|
|
} {
|
|
r := aMeshReadyToMove()
|
|
c.break_(&r)
|
|
why := NotReady(r)
|
|
if len(why) == 0 {
|
|
t.Errorf("%s did not stop the move", c.what)
|
|
continue
|
|
}
|
|
if !strings.Contains(strings.Join(why, "\n"), c.says) {
|
|
t.Errorf("%s: the refusal does not mention %q: %v", c.what, c.says, why)
|
|
}
|
|
}
|
|
}
|
|
|
|
// What the move would do is written out rather than summarised, because this is the one step with
|
|
// nothing to inspect afterwards — so reading it is the last chance to disagree.
|
|
func TestWhatMovesNamesEveryMachineAndSaysTheOldBrokerStays(t *testing.T) {
|
|
r := aMeshReadyToMove()
|
|
r.OldBusHasOtherClients = true
|
|
steps := strings.Join(WhatMoves(r), "\n")
|
|
|
|
for _, want := range []string{"anchor", "laptop", "user list", "module runtime"} {
|
|
if !strings.Contains(steps, want) {
|
|
t.Errorf("the plan does not mention %q:\n%s", want, steps)
|
|
}
|
|
}
|
|
// Said explicitly, so nobody reads the move as switching the old broker off — it stays serving
|
|
// whatever else uses it, and that is a decision already taken.
|
|
if !strings.Contains(steps, "not its retirement") {
|
|
t.Errorf("the plan does not say the old broker stays:\n%s", steps)
|
|
}
|
|
}
|