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.
159 lines
6.0 KiB
Go
159 lines
6.0 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A module whose files cannot be written in advance.
|
|
//
|
|
// The mesh's private network is the case: a machine's peer list is derived from every other
|
|
// machine, so it differs on each one and changes when any of them changes. What matters in these
|
|
// tests is not that it works, but that being computed changes *nothing else* about being a
|
|
// module — it is assigned, resolved, settled and absent when nobody asked for it.
|
|
|
|
type fake struct {
|
|
on map[string]bool
|
|
asked []string
|
|
err error
|
|
}
|
|
|
|
func (f *fake) Resources(node string) ([]map[string]any, bool, error) {
|
|
f.asked = append(f.asked, node)
|
|
if f.err != nil {
|
|
return nil, false, f.err
|
|
}
|
|
if !f.on[node] {
|
|
return nil, false, nil
|
|
}
|
|
return []map[string]any{{"id": "peers", "type": "file", "path": "/etc/x.conf",
|
|
"merge": MergeJSON, "content": `{"peer":"` + node + `"}`}}, true, nil
|
|
}
|
|
|
|
func computedShelf() map[string]Manifest {
|
|
return shelf(Manifest{Module: "mesh-network", Computed: "mesh-network",
|
|
Provides: []string{"private-network"}})
|
|
}
|
|
|
|
func TestAComputedModuleIsAskedAboutTheNodeItIsFor(t *testing.T) {
|
|
// The generator gets a node name, not a plan. Everything it needs is the whole mesh, which
|
|
// it was built with — this is the one thing a node could never work out for itself.
|
|
got, err := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gen.asked) != 1 || gen.asked[0] != "workstation" {
|
|
t.Fatalf("asked about %v, wanted workstation once", gen.asked)
|
|
}
|
|
if len(out) != 1 || !strings.Contains(out[0]["content"].(string), `"peer": "workstation"`) {
|
|
t.Fatalf("got %v", out)
|
|
}
|
|
}
|
|
|
|
func TestAMachineNobodyGaveItGetsNothing(t *testing.T) {
|
|
// The whole point of making the network a module. Before this, every machine with an address
|
|
// was on the private network and there was no way to say one should stay off.
|
|
got, err := Resolve(computedShelf(), nil, workstation(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out) != 0 {
|
|
t.Fatalf("a machine that was assigned nothing got %d resource(s)", len(out))
|
|
}
|
|
if len(gen.asked) != 0 {
|
|
t.Fatalf("the generator was asked about %v, and nobody had asked for it", gen.asked)
|
|
}
|
|
}
|
|
|
|
func TestAssignedAndNotYetPartOfItIsNotAFailure(t *testing.T) {
|
|
// A machine given the network module before it has a place on it. Brief and ordinary — the
|
|
// answer is "nothing yet", and treating it as an error would make an ordering a fault.
|
|
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), nil)
|
|
gen := &fake{on: map[string]bool{}}
|
|
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
|
if err != nil {
|
|
t.Fatalf("refused a node that is not on the network yet: %v", err)
|
|
}
|
|
if len(out) != 0 {
|
|
t.Fatalf("got %v", out)
|
|
}
|
|
}
|
|
|
|
func TestAGeneratorThisControlPlaneDoesNotHaveIsRefused(t *testing.T) {
|
|
// Sending a machine a module with no files would look like it worked. Named in the message,
|
|
// because the only fix is a control plane that has it.
|
|
got, _ := Resolve(shelf(Manifest{Module: "weather", Computed: "the-weather"}),
|
|
[]string{"weather"}, workstation(), nil)
|
|
_, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": &fake{}}})
|
|
if err == nil {
|
|
t.Fatal("a module computed by nothing was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "the-weather") {
|
|
t.Fatalf("the refusal does not name what is missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAModuleIsEitherWrittenOrComputedNotBoth(t *testing.T) {
|
|
// Otherwise nobody could say where a given file on a machine came from.
|
|
_, err := ParseManifest([]byte(`{"module":"mesh-network","version":"1",
|
|
"computed":"mesh-network",
|
|
"resources":[{"id":"a","type":"package","package":"wireguard-tools"}]}`))
|
|
if err == nil {
|
|
t.Fatal("a module that both ships files and has them computed was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "one or the other") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSettingsApplyToAComputedModuleToo(t *testing.T) {
|
|
// Being computed is about where the files come from, not about whether they are configurable.
|
|
// A line drawn there would be arbitrary and nobody could predict it.
|
|
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), nil)
|
|
gen := &fake{on: map[string]bool{"workstation": true}}
|
|
out, err := got.Declaration(Rendering{
|
|
Generators: map[string]Generator{"mesh-network": gen},
|
|
Settings: SettingsBy{"mesh-network": {{From: "the mesh",
|
|
Values: map[string]any{"keepalive": 25}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := out[0]["content"].(string)
|
|
if !strings.Contains(content, `"keepalive": 25`) {
|
|
t.Fatalf("the setting did not reach a computed file: %s", content)
|
|
}
|
|
if !strings.Contains(content, `"peer": "workstation"`) {
|
|
t.Fatalf("the setting replaced what the generator computed: %s", content)
|
|
}
|
|
}
|
|
|
|
func TestTwoWaysToBeOnAPrivateNetworkRefuseAndNameBoth(t *testing.T) {
|
|
// WireGuard is one way. The refusing rule is what makes a second one safe to add: assigning
|
|
// both is caught rather than producing a machine on two networks that each half-work.
|
|
_, err := Resolve(shelf(
|
|
Manifest{Module: "mesh-network", Computed: "mesh-network",
|
|
Provides: []string{"private-network"}},
|
|
Manifest{Module: "tailscale", Provides: []string{"private-network"}},
|
|
Manifest{Module: "backups", Requires: []string{"private-network"}},
|
|
), []string{"backups"}, workstation(), nil)
|
|
if err == nil {
|
|
t.Fatal("two answers to one requirement were taken silently")
|
|
}
|
|
for _, want := range []string{"mesh-network", "tailscale", "private-network"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("the refusal does not name %s: %v", want, err)
|
|
}
|
|
}
|
|
}
|