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
118 lines
4.3 KiB
Go
118 lines
4.3 KiB
Go
package catalogue_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/overlay"
|
|
)
|
|
|
|
// The manifests the control plane actually ships, resolved.
|
|
//
|
|
// Written because the earlier tests built their own manifests and passed while the real one was
|
|
// missing a claim — a whole mechanism could have been absent from what ships and every test would
|
|
// still have been green.
|
|
|
|
func provided(t *testing.T) map[string]catalogue.Manifest {
|
|
t.Helper()
|
|
out := map[string]catalogue.Manifest{}
|
|
for _, raw := range []map[string]any{
|
|
overlay.Manifest(), overlay.DomainManifest(),
|
|
} {
|
|
b, err := json.Marshal(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := catalogue.ParseManifest(b)
|
|
if err != nil {
|
|
t.Fatalf("a manifest this control plane ships is not valid: %v", err)
|
|
}
|
|
out[m.Module] = m
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestTheShippedNetworkingModulesResolveOnTheirOwn(t *testing.T) {
|
|
got, err := catalogue.Resolve(provided(t), []string{overlay.Domain}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
|
|
|
if err != nil {
|
|
t.Fatalf("assigning %s does not work out of the box: %v", overlay.Domain, err)
|
|
}
|
|
var have []string
|
|
for _, m := range got.Modules {
|
|
have = append(have, m.Module)
|
|
}
|
|
for _, want := range []string{overlay.Domain, overlay.Name} {
|
|
if !strings.Contains(strings.Join(have, " "), want) {
|
|
t.Fatalf("%s did not bring in %s: %v", overlay.Domain, want, have)
|
|
}
|
|
}
|
|
|
|
// **The names come WITH the network now, not from a third module.** Being on the private
|
|
// network is what gives a machine a name, so the provider asks for the node-names fact and
|
|
// there is nothing else to bring in. A module that ran nothing used to be here.
|
|
for _, m := range got.Modules {
|
|
if m.Module == overlay.Name && m.Facts["node-names"] == "" {
|
|
t.Fatalf("the network's provider does not ask for the names: %+v", m.Facts)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnotherVPNSatisfiesNetworkingWithoutDraggingWireGuardIn(t *testing.T) {
|
|
// **This inverted, and the inversion is the improvement.** The names used to be a module that
|
|
// required the mesh's own addressing, which only WireGuard provided — so choosing another VPN
|
|
// dragged WireGuard in anyway, and the node-scoped claim existed to at least make that
|
|
// collision loud. With the names a fact rather than a provision, a person who chose tailscale
|
|
// gets tailscale, and there is nothing left to collide.
|
|
shipped := provided(t)
|
|
got, err := catalogue.Resolve(
|
|
withTailscale(shipped),
|
|
[]string{overlay.Domain, "tailscale"},
|
|
catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
|
if err != nil {
|
|
t.Fatalf("choosing another VPN was refused: %v", err)
|
|
}
|
|
for _, m := range got.Modules {
|
|
if m.Module == overlay.Name {
|
|
t.Fatalf("the other VPN was chosen and WireGuard came anyway: %v", got.Modules)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTwoVPNsAssignedTogetherStillCollide(t *testing.T) {
|
|
// The claim still guards the case it was always for: both assigned EXPLICITLY, which is a
|
|
// machine with two private networks and a coin toss about which one a peer reaches it on.
|
|
shipped := provided(t)
|
|
_, err := catalogue.Resolve(
|
|
withTailscale(shipped),
|
|
[]string{overlay.Name, "tailscale"},
|
|
catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
|
if err == nil {
|
|
t.Fatal("a machine was given two private networks and nobody was told")
|
|
}
|
|
if !strings.Contains(err.Error(), overlay.TheNetwork) {
|
|
t.Fatalf("the refusal does not say what collided: %v", err)
|
|
}
|
|
}
|
|
|
|
// The names-need-addressing test went with the names module: names are a fact now, and a machine
|
|
// the mesh cannot place is simply left out of the file (facts_test.go) — which is the same
|
|
// protection, enforced where the file is written rather than by a provision refusing.
|
|
|
|
func withTailscale(shelf map[string]catalogue.Manifest) map[string]catalogue.Manifest {
|
|
out := map[string]catalogue.Manifest{}
|
|
for k, v := range shelf {
|
|
out[k] = v
|
|
}
|
|
// Deliberately without name-resolution of its own, which is the case that used to install
|
|
// both VPNs: the names then needed the mesh's addressing, and only WireGuard has it.
|
|
out["tailscale"] = catalogue.Manifest{
|
|
Module: "tailscale", Version: "1",
|
|
Provides: catalogue.Offers(overlay.Requirement),
|
|
Claims: []catalogue.Claim{{Name: overlay.TheNetwork, Scope: catalogue.ScopeNode}},
|
|
}
|
|
return out
|
|
}
|