Steps 6 to 10, which turn a substrate into a mesh that can maintain itself
(novox/hq ADR 0067).
6 enrol a node record, a token, `mesh-host enrol`, and the host agent
running. Proved by the mesh having HEARD from the node, not by a
process existing: a host that cannot reach the broker looks exactly
like a successful install until the first push applies nothing.
7 registry the module that gives this mesh an image store, registered from a
--catalog checkout, assigned and pushed. Its image is upstream and
never built (04-ISSUES/029) — a placeholder digest there is refused.
Verified by asking `/v2/`, because a container that is up is not a
registry that serves.
8 publish the carried image pushed into that registry, which assigns it the
first manifest digest it has ever had. This is the hinge: without
it the mesh works and can never upgrade itself.
9 control the control plane registered as an ordinary module pinned to that
digest, with the substrate's own store connections delivered
through `secret accept` — read out of the bundle that made them,
because the mesh cannot invent a credential that predates it.
10 retire the temporary control plane dropped from the bundle and removed by
the host's ordinary removal pass.
Every step asks before it acts and reports "already done". No step leaves the
machine without a control plane: steps 9 and 10 overlap deliberately, and two
stateless control planes are untidy rather than broken.
mesh-control's `internal/builder`.PublishImage is mirrored rather than imported —
tier 0 depends on nothing that must be installed first — with one correction: the
digest is chosen from RepoDigests by repository instead of taken as element zero,
so an image pushed to two registries cannot silently pin this mesh to the wrong
one.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
224 lines
8.3 KiB
Go
224 lines
8.3 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
|
|
}
|
|
return "", fmt.Errorf("unexpected: %s %v", name, args)
|
|
}}
|
|
|
|
out, err := Enrol(context.Background(), Options{
|
|
Node: "anchor", State: alreadyEnrolled(t, "anchor"), Timeout: time.Second,
|
|
}, arch(t), controlPlane{container: "temp-mesh-control", 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)
|
|
}
|
|
}
|
|
|
|
// 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-control", 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-control", 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-control", 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-control", 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")
|
|
}
|
|
}
|