/etc/hosts is the file of the node-hosts-file seat's holder; the controller writes into no file another seat's holder owns, and asks that holder if it ever needs a line there. The private network's module stops asking for the node-names fact; every machine already asks the mesh's one resolver for these names, and the host gives the region back at the next push.
120 lines
4.3 KiB
Go
120 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 network writes no names** (novox/hq ADR 0199): /etc/hosts is the hosts seat holder's
|
|
// file, and the mesh's resolver answers the machines' names. No fact of the network's module
|
|
// may name that file.
|
|
for _, m := range got.Modules {
|
|
for name, f := range m.Facts {
|
|
if m.Module == overlay.Name && f.Path == "/etc/hosts" {
|
|
t.Fatalf("the network's provider still writes /etc/hosts, as its %q fact", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|