One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
264 lines
10 KiB
Go
264 lines
10 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// Step 6 is where the mesh stops being something running on a machine and starts being something
|
|
// the machine belongs to. What these tests defend is that it cannot happen twice, and that
|
|
// "installed" is never claimed for a machine the mesh has not actually heard from.
|
|
|
|
// alreadyEnrolled writes an identity file, as `mesh-host enrol` leaves behind.
|
|
func alreadyEnrolled(t *testing.T, node string) string {
|
|
t.Helper()
|
|
state := filepath.Join(t.TempDir(), "state.json")
|
|
mine, err := identity.Generate(node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A whole membership, because an identity that cannot reach its mesh is refused on the way in
|
|
// — which is the right refusal and not the one being tested here.
|
|
signer, _, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mine.Membership = identity.Membership{
|
|
Broker: "192.0.2.10:5671", Fingerprint: "sha256:whatever",
|
|
Signer: signer, Password: "issued-at-enrolment",
|
|
}
|
|
if err := identity.Save(identity.Path(state), mine); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return state
|
|
}
|
|
|
|
func arch(t *testing.T) system.System {
|
|
t.Helper()
|
|
chosen, err := system.For("arch")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return chosen
|
|
}
|
|
|
|
// A machine that has already enrolled is not enrolled again, and no token is spent on it. The
|
|
// installer is run over and over; a second identity is one the mesh does not know, and the mesh
|
|
// believes the first.
|
|
func TestAMachineThatHasAlreadyEnrolledIsNotEnrolledAgain(t *testing.T) {
|
|
runtime := &asked{answer: func(name string, args []string) (string, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case strings.Contains(joined, "node list"):
|
|
return "anchor here 01J0\n", nil
|
|
// An agent is running here too. Both halves are needed: enrolling makes the mesh hear from
|
|
// a machine once, so the first answer alone also describes a machine with no agent at all.
|
|
case name == "pgrep":
|
|
return "4242\n", nil
|
|
}
|
|
return "", fmt.Errorf("unexpected: %s %v", name, args)
|
|
}}
|
|
|
|
out, err := Enrol(context.Background(), Options{
|
|
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
|
|
Host: "/usr/local/bin/mesh-host", HostInBackground: true,
|
|
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
|
func(string) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Joined || out.Added {
|
|
t.Error("a machine that had already enrolled enrolled again")
|
|
}
|
|
if runtime.ran("token issue") {
|
|
t.Errorf("a token was issued for a machine that already holds an identity: %v",
|
|
runtime.commands)
|
|
}
|
|
if out.Agent != "already running" {
|
|
t.Errorf("the host agent is reported as %q", out.Agent)
|
|
}
|
|
}
|
|
|
|
// The mesh having heard from a machine is not the same as an agent running on it, and enrolling is
|
|
// itself the thing that makes the mesh hear. Taken as proof of life it ends the step believing an
|
|
// agent it never started, and everything after is the control plane being told things that never
|
|
// reach the machine: the next push is accepted, recorded, and applied by nobody.
|
|
func TestAMeshThatHasHeardFromAMachineWithNoAgentStartsOne(t *testing.T) {
|
|
runtime := &asked{answer: func(name string, args []string) (string, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case strings.Contains(joined, "node list"):
|
|
// Exactly what enrolling leaves behind, with nothing running.
|
|
return "anchor here 01J0\n", nil
|
|
case name == "pgrep":
|
|
return "", fmt.Errorf("exit status 1")
|
|
case name == "sh":
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("unexpected: %s %v", name, args)
|
|
}}
|
|
|
|
out, err := Enrol(context.Background(), Options{
|
|
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
|
|
Host: "/usr/local/bin/mesh-host", HostInBackground: true,
|
|
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
|
func(string) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Agent == "already running" {
|
|
t.Fatal("a machine with no agent was reported as already running it")
|
|
}
|
|
if !runtime.ran("nohup") {
|
|
t.Errorf("no agent was started on a machine that has none: %v", runtime.commands)
|
|
}
|
|
}
|
|
|
|
// A machine already enrolled under ANOTHER name is refused, with what to do about it. Re-enrolling
|
|
// replaces the identity the mesh recorded, which is a deliberate act and not something an
|
|
// installer does on its own.
|
|
func TestAMachineEnrolledUnderAnotherNameIsRefused(t *testing.T) {
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
if strings.Contains(strings.Join(args, " "), "node list") {
|
|
return "somewhere-else here 01J0\n", nil
|
|
}
|
|
return "", nil
|
|
}}
|
|
|
|
_, err := Enrol(context.Background(), Options{
|
|
Node: "anchor", State: alreadyEnrolled(t, "somewhere-else"), Timeout: time.Second,
|
|
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
|
func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a machine already enrolled as something else was enrolled again")
|
|
}
|
|
for _, wanted := range []string{"somewhere-else", "--node"} {
|
|
if !strings.Contains(err.Error(), wanted) {
|
|
t.Errorf("the refusal does not mention %q:\n%v", wanted, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The mesh having heard from the node is the proof, not a process existing.** A host that is
|
|
// running and cannot reach the broker looks exactly like a successful install until the first push
|
|
// silently applies nothing — which is the class of fault this whole program exists to stop being
|
|
// found late.
|
|
func TestAHostThatIsRunningAndUnheardOfIsNotAnInstall(t *testing.T) {
|
|
previous := answerEvery
|
|
answerEvery = time.Millisecond
|
|
defer func() { answerEvery = previous }()
|
|
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case strings.Contains(joined, "node list"):
|
|
// Enrolled, and never spoken.
|
|
return "anchor never spoken 01J0\n", nil
|
|
case args[0] == "show":
|
|
return "LoadState=loaded\nActiveState=active\n", nil
|
|
case args[0] == "is-enabled":
|
|
return "enabled\n", nil
|
|
}
|
|
return "", nil
|
|
}}
|
|
|
|
_, err := Enrol(context.Background(), Options{
|
|
Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service",
|
|
Timeout: time.Second, Wait: 0,
|
|
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
|
func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a node the mesh has never heard from was reported enrolled and running")
|
|
}
|
|
if !strings.Contains(err.Error(), "MESH_BROKER_ADDRESS") {
|
|
t.Errorf("the failure does not name the thing that is silently fatal when wrong:\n%v", err)
|
|
}
|
|
}
|
|
|
|
// A machine with no service to start is refused, and the refusal says what is missing rather than
|
|
// inventing a unit file. What a unit says is a packaging decision, and an installer writing one
|
|
// would put a file on the machine that whatever installed the host will disagree with.
|
|
func TestAMachineWithNoHostServiceIsRefusedRatherThanGivenOne(t *testing.T) {
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case strings.Contains(joined, "node list"):
|
|
return "anchor never spoken 01J0\n", nil
|
|
case args[0] == "show":
|
|
// systemd knows nothing about it.
|
|
return "LoadState=not-found\nActiveState=inactive\n", nil
|
|
}
|
|
return "", nil
|
|
}}
|
|
|
|
_, err := Enrol(context.Background(), Options{
|
|
Node: "anchor", State: alreadyEnrolled(t, "anchor"), HostService: "mesh-host.service",
|
|
Timeout: time.Second, Wait: 0,
|
|
}, arch(t), controlPlane{container: "temp-mesh-controller", run: runtime.run, timeout: time.Second},
|
|
func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a machine with no host service was reported as having a running host")
|
|
}
|
|
for _, wanted := range []string{"mesh-host.service", "--host-in-background"} {
|
|
if !strings.Contains(err.Error(), wanted) {
|
|
t.Errorf("the refusal does not mention %q:\n%v", wanted, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A machine with no name is refused before anything is said to the mesh. The name is what the
|
|
// record, the token, the assignment and the push all name, and one the installer invented would
|
|
// match nothing anybody types anywhere else.
|
|
func TestAMachineWithNoNameIsRefusedBeforeAnythingIsAsked(t *testing.T) {
|
|
runtime := &asked{answer: func(string, []string) (string, error) {
|
|
return "", fmt.Errorf("nothing should have been asked")
|
|
}}
|
|
_, err := Enrol(context.Background(), Options{Timeout: time.Second}, arch(t),
|
|
controlPlane{container: "temp-mesh-controller", run: runtime.run}, func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a machine with no name was enrolled")
|
|
}
|
|
if len(runtime.commands) != 0 {
|
|
t.Errorf("the mesh was asked something first: %v", runtime.commands)
|
|
}
|
|
}
|
|
|
|
// The token is found in what the mesh said, by the rule the lab uses: the one long unbroken line.
|
|
// The installer does not parse a format the control plane owns.
|
|
func TestTheTokenIsFoundInWhatTheMeshSaid(t *testing.T) {
|
|
said := "a token for anchor, good once:\n\n " + strings.Repeat("t", 240) + "\n\n" +
|
|
"carry it to the machine and run: mesh-host enrol --token <token>\n"
|
|
token, err := tokenIn(said)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if token != strings.Repeat("t", 240) {
|
|
t.Errorf("the token was read as %q", token)
|
|
}
|
|
|
|
if _, err := tokenIn("nothing here that looks like one\n"); err == nil {
|
|
t.Fatal("an answer with no token in it was accepted")
|
|
}
|
|
}
|
|
|
|
// A listing's name is matched as a whole word at the start of a line, so `registry` is not found
|
|
// inside `registry-mirror`. A substring match would report a module installed that is not, and the
|
|
// installer would skip creating it.
|
|
func TestAListingIsMatchedByNameAndNotBySubstring(t *testing.T) {
|
|
listing := "registry-mirror 1 built abc\nother 1 built def\n"
|
|
if mentions(listing, "registry") {
|
|
t.Error("registry-mirror was read as registry")
|
|
}
|
|
if !mentions(listing, "registry-mirror") {
|
|
t.Error("registry-mirror was not found")
|
|
}
|
|
}
|