Files
mesh-host/internal/bootstrap/enrol_test.go
jschoubben 121367319d Rename mesh-control -> mesh-controller, substrate -> foundation
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
2026-09-16 18:40:40 +02:00

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")
}
}