Files
mesh-controller/internal/catalogue/provided_test.go
T
jschoubben 44d134ba25 Networking is a module, and a domain module is how you avoid choosing
Connectivity was code beside the module system doing the module system's
job: every machine with an address was on the private network and there
was no way to keep one off.

A manifest can now say its resources are computed by the control plane,
which is what a peer list needs — it is derived from every machine at
once, so nothing could be written in advance. The network is a module
from there on: assigned, resolved, settled, and absent from a machine
nobody gave it to.

Three modules rather than one, because WireGuard is one VPN of several:

  mesh-wireguard   provides private-network, mesh-addressing
                   claims the-private-network, one per node
  mesh-names       provides name-resolution, requires mesh-addressing
  networking       requires both, and ships no files of its own

The last is the point. Most people want the network up and do not want
to choose a VPN, so `assign networking` takes the only answer to each
requirement silently. The day the catalogue holds a second one there are
two answers, the resolver refuses and names them, and choosing is
assigning the one you want. No flavor field, nothing to configure.

Names left the WireGuard declaration for their own module. They would be
identical over a different private network, and bundling them made one
module out of two things.

Three faults the walk found:

- choosing tailscale still installed WireGuard, dragged back in by the
  names needing the mesh's own addresses. Caught now by a claim: running
  two VPNs is fine, being *the* mesh network is singular.
- a requirement wanted by two modules was reported twice, identically.
- "this mesh has no hub" was reported when the real cause was that a
  node could not be resolved at all. It now names the node and the why.

And a test that asserts the manifests actually shipped, after the claim
went missing from the real one while every test stayed green.
2026-08-29 23:19:32 +02:00

99 lines
3.3 KiB
Go

package catalogue_test
import (
"encoding/json"
"strings"
"testing"
"github.com/novox/mesh-control/internal/catalogue"
"github.com/novox/mesh-control/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.NamesManifest(), 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"}, nil)
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, overlay.Names} {
if !strings.Contains(strings.Join(have, " "), want) {
t.Fatalf("%s did not bring in %s: %v", overlay.Domain, want, have)
}
}
}
func TestTheShippedWireGuardModuleClaimsBeingTheNetwork(t *testing.T) {
// Without this, a person who chose another VPN gets WireGuard as well, dragged in by the
// names, and is not told. The claim is the only thing that catches it.
shipped := provided(t)
_, err := catalogue.Resolve(
withTailscale(shipped),
[]string{overlay.Domain, "tailscale"},
catalogue.Node{Name: "workstation", Site: "house"}, nil)
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)
}
}
func TestTheShippedNamesModuleNeedsTheMeshsOwnAddresses(t *testing.T) {
// Over a VPN whose addresses the mesh does not hand out, it has no names to write. Refusing
// is what stops a machine getting a hosts file that means nothing on it.
shipped := provided(t)
delete(shipped, overlay.Name)
_, err := catalogue.Resolve(shipped, []string{overlay.Names},
catalogue.Node{Name: "workstation", Site: "house"}, nil)
if err == nil {
t.Fatal("the mesh's names resolved with nothing handing out the mesh's addresses")
}
if !strings.Contains(err.Error(), overlay.Addressing) {
t.Fatalf("the refusal does not name what is missing: %v", err)
}
}
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: []string{overlay.Requirement},
Claims: []catalogue.Claim{{Name: overlay.TheNetwork, Scope: catalogue.ScopeNode}},
}
return out
}