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
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
package link_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// What a node says when it joins, as that node's own code writes it.
|
|
//
|
|
// The two ends are separate structs in separate repositories, and a field renamed on one side
|
|
// fails silently: enrolment succeeds, a key is simply absent, and the node looks joined until the
|
|
// first thing sealed to it cannot be opened — by which time nobody is looking at enrolment.
|
|
//
|
|
// Skipped unless MESH_ENROL names the file the host's suite wrote:
|
|
//
|
|
// mesh-host: MESH_ENROL_OUT=/tmp/enrol.json go test ./internal/link/
|
|
// mesh-controller: MESH_ENROL=/tmp/enrol.json make check
|
|
//
|
|
// **What this does not cover**, said so nobody reads more into a pass than is there: the full
|
|
// enrolment path also issues a broker account, and that needs a broker. What is checked here is
|
|
// the shape the two sides agree on and that a key which arrives this way can actually be sealed
|
|
// to — which is the part that was newly wired and the part that fails quietly.
|
|
func TestWhatANodeSaysWhenItJoinsIsWhatThisMeshReads(t *testing.T) {
|
|
path := os.Getenv("MESH_ENROL")
|
|
if path == "" {
|
|
t.Skip("set MESH_ENROL to an enrolment request written by the host's suite")
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var request link.EnrolRequest
|
|
if err := json.Unmarshal(raw, &request); err != nil {
|
|
t.Fatalf("this mesh cannot read what a node sends:\n%v", err)
|
|
}
|
|
for what, got := range map[string]string{
|
|
"node": request.Node,
|
|
"secret": request.Secret,
|
|
"overlay key": request.OverlayKey,
|
|
"sealing key": request.SealingKey,
|
|
} {
|
|
if got == "" {
|
|
t.Fatalf("the %s did not survive the crossing — a field name differs", what)
|
|
}
|
|
}
|
|
if len(request.PublicKey) != ed25519.PublicKeySize {
|
|
t.Fatalf("the identity arrived as %d bytes", len(request.PublicKey))
|
|
}
|
|
|
|
// And that a key arriving this way is one the mesh can actually seal to. Recording it is not
|
|
// the same as it being usable, and "recorded" is what a shape check on its own would prove.
|
|
inv := liveInventory(t)
|
|
ctx := context.Background()
|
|
node, err := inv.AddNode(ctx, request.Node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := inv.AddNode(ctx, "the-other-end")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordSealingKey(ctx, node.ID, request.SealingKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordSealingKey(ctx, other.ID, request.SealingKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A credential belongs to a module on a machine (novox/hq 04-ISSUES/022), so the module
|
|
// holding it has to exist before it can.
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "gitea", Version: "1"},
|
|
inventory.Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
secret, err := inv.SecretFor(ctx, "postgres-database", request.Node, "gitea", "the-other-end")
|
|
if err != nil {
|
|
t.Fatalf("nothing could be sealed to a key that arrived from a real node: %v", err)
|
|
}
|
|
|
|
// Opened with the private half the host's suite kept, so this asserts the node could read it
|
|
// rather than that a blob exists.
|
|
privateRaw, err := os.ReadFile(path + ".sealing-private")
|
|
if err != nil {
|
|
t.Skipf("no private half beside %s, so this can only check the shape", path)
|
|
}
|
|
private, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(privateRaw)))
|
|
if err != nil || len(private) != 32 {
|
|
t.Fatal("the private half beside the request is not a key")
|
|
}
|
|
public, err := base64.StdEncoding.DecodeString(request.SealingKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pub, priv [32]byte
|
|
copy(pub[:], public)
|
|
copy(priv[:], private)
|
|
blob, err := base64.StdEncoding.DecodeString(secret.ForConsumer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := box.OpenAnonymous(nil, blob, &pub, &priv); !ok {
|
|
t.Fatal("the node could not open what this mesh sealed to the key it sent")
|
|
}
|
|
}
|
|
|
|
// liveInventory is a database of its own for this test, skipping where there is none.
|
|
func liveInventory(t *testing.T) *inventory.Inventory {
|
|
t.Helper()
|
|
if os.Getenv("MESH_TEST_POSTGRES") == "" {
|
|
t.Skip("no MESH_TEST_POSTGRES; run `make check` to raise one")
|
|
}
|
|
return inventory.ForTest(t)
|
|
}
|