networking required mesh-wireguard and nothing else; machines are assigned the network directly. module forget refuses a provided module, so a retired one is removed at start once no machine has it. Guard route-proxy's public account directory against a reissue.
145 lines
5.6 KiB
Go
145 lines
5.6 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(),
|
|
} {
|
|
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 TestTheShippedPrivateNetworkResolvesOnItsOwn(t *testing.T) {
|
|
// **Assigned directly** (novox/hq ADR 0226): the `networking` bundle that required it is
|
|
// retired, so the private network's own module is what a machine is given, and it must need
|
|
// nothing the control plane does not ship beside it.
|
|
got, err := catalogue.Resolve(provided(t), []string{overlay.Name}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{})
|
|
if err != nil {
|
|
t.Fatalf("assigning %s does not work out of the box: %v", overlay.Name, err)
|
|
}
|
|
if len(got.Modules) != 1 || got.Modules[0].Module != overlay.Name {
|
|
t.Fatalf("assigning %s brought %v", overlay.Name, got.Modules)
|
|
}
|
|
|
|
// **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 f.Path == "/etc/hosts" {
|
|
t.Fatalf("the network's provider still writes /etc/hosts, as its %q fact", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheControlPlaneShipsNoNetworkingBundle(t *testing.T) {
|
|
// ADR 0226: one module for the one private network. A bundle shipped beside it again would be
|
|
// a second name every machine carries for the same thing.
|
|
shipped := provided(t)
|
|
if len(shipped) != 1 {
|
|
t.Fatalf("the control plane ships %d modules, want only %s", len(shipped), overlay.Name)
|
|
}
|
|
if _, ok := shipped["networking"]; ok {
|
|
t.Fatal("the retired networking bundle is shipped again")
|
|
}
|
|
}
|
|
|
|
func TestARefusalForWantOfThePrivateNetworkNamesItsModule(t *testing.T) {
|
|
// The hint in a refusal is a string in the resolver rather than an import of this package. It
|
|
// named `networking` until ADR 0226; a hint naming a module nobody ships sends a person to
|
|
// assign something that does not exist.
|
|
shelf := map[string]catalogue.Manifest{
|
|
"app": {Module: "app", Version: "1", Requires: []string{"postgres-database"}},
|
|
"postgres": {Module: "postgres", Version: "1", Provides: catalogue.FromAnywhere("postgres-database")},
|
|
}
|
|
_, err := catalogue.Resolve(shelf, []string{"app"}, catalogue.Node{Name: "workstation"},
|
|
catalogue.World{Offered: map[string][]catalogue.Provider{
|
|
"postgres-database": {{Node: "anchor", At: "anchor.internal"}}}})
|
|
if err == nil {
|
|
t.Fatal("an app off the private network was pointed at a database on it")
|
|
}
|
|
if !strings.Contains(err.Error(), "assign "+overlay.Name) {
|
|
t.Fatalf("the refusal does not name the private network's module %s: %v", overlay.Name, err)
|
|
}
|
|
}
|
|
|
|
func TestAnotherVPNIsChosenByAssigningItInstead(t *testing.T) {
|
|
// What the bundle was for, kept without it: a machine given another VPN answers
|
|
// private-network from that VPN, and WireGuard is not dragged in by anything.
|
|
shipped := provided(t)
|
|
shelf := withTailscale(shipped)
|
|
shelf["needs-network"] = catalogue.Manifest{Module: "needs-network", Version: "1",
|
|
Requires: []string{overlay.Requirement}}
|
|
got, err := catalogue.Resolve(shelf, []string{"needs-network", "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
|
|
}
|