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.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A domain module is a module with requirements and no files of its own.
|
||||
//
|
||||
// Most people want the network working and do not want to choose a VPN. Some want a particular
|
||||
// one. Both are the same mechanism: assigning `networking` takes the only answer to each of its
|
||||
// requirements silently, and the day there are two answers the resolver refuses and names them,
|
||||
// so choosing is assigning the one you want. There is no flavor field and nothing to configure.
|
||||
|
||||
func networkingShelf(extra ...Manifest) map[string]Manifest {
|
||||
base := []Manifest{
|
||||
{Module: "networking", Requires: []string{"private-network", "name-resolution"}},
|
||||
{Module: "mesh-wireguard", Computed: "mesh-wireguard",
|
||||
Provides: []string{"private-network", "mesh-addressing"},
|
||||
Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}},
|
||||
{Module: "mesh-names", Computed: "mesh-names",
|
||||
Provides: []string{"name-resolution"}, Requires: []string{"mesh-addressing"}},
|
||||
}
|
||||
return shelf(append(base, extra...)...)
|
||||
}
|
||||
|
||||
func TestOneWordBringsUpTheNetwork(t *testing.T) {
|
||||
// The case that has to stay easy. Nothing is asked, because with one answer to each
|
||||
// requirement there was never a question.
|
||||
got, err := Resolve(networkingShelf(), []string{"networking"}, workstation(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
have := strings.Join(names(got), " ")
|
||||
for _, want := range []string{"networking", "mesh-wireguard", "mesh-names"} {
|
||||
if !strings.Contains(have, want) {
|
||||
t.Fatalf("assigning networking did not bring in %s: %s", want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecondVPNTurnsItIntoAChoice(t *testing.T) {
|
||||
// And the choice is offered rather than made. A default here would be the flavor field coming
|
||||
// back under another name.
|
||||
_, err := Resolve(networkingShelf(
|
||||
Manifest{Module: "tailscale", Provides: []string{"private-network"},
|
||||
Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}},
|
||||
), []string{"networking"}, workstation(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("two VPNs and one was picked silently")
|
||||
}
|
||||
for _, want := range []string{"mesh-wireguard", "tailscale"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("the refusal does not name %s: %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChoosingIsAssigning(t *testing.T) {
|
||||
// No second verb. Assigning the one you want answers the requirement, and the bundle takes it.
|
||||
got, err := Resolve(networkingShelf(
|
||||
Manifest{Module: "tailscale",
|
||||
Provides: []string{"private-network", "name-resolution"},
|
||||
Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}},
|
||||
), []string{"networking", "tailscale"}, workstation(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
have := strings.Join(names(got), " ")
|
||||
if !strings.Contains(have, "tailscale") {
|
||||
t.Fatalf("the chosen VPN is not in the set: %s", have)
|
||||
}
|
||||
if strings.Contains(have, "mesh-wireguard") {
|
||||
t.Fatalf("choosing tailscale still installed WireGuard: %s", have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChoosingOneVPNCannotDragTheOtherBackIn(t *testing.T) {
|
||||
// This happened. The person chose tailscale; the names module required the mesh's own
|
||||
// addressing; only WireGuard provides that; so both were installed and nobody was told.
|
||||
//
|
||||
// The claim is what catches it. Providing a private network is not the singular part — a
|
||||
// machine could run two VPNs for two purposes — but being *the* one the mesh runs over is.
|
||||
_, err := Resolve(networkingShelf(
|
||||
Manifest{Module: "tailscale", Provides: []string{"private-network"},
|
||||
Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}},
|
||||
), []string{"networking", "tailscale"}, workstation(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("a machine was given two private networks without being told")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "the-private-network") {
|
||||
t.Fatalf("the refusal does not say what collided: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamesNeedTheMeshsOwnAddresses(t *testing.T) {
|
||||
// Names are computed from addresses the mesh handed out. Over a VPN that hands out its own,
|
||||
// the mesh has nothing to write, so the names module requires the addressing rather than a
|
||||
// private network in general — otherwise a machine gets a hosts file full of addresses that
|
||||
// mean nothing on it.
|
||||
_, err := Resolve(shelf(
|
||||
Manifest{Module: "mesh-names", Computed: "mesh-names",
|
||||
Provides: []string{"name-resolution"}, Requires: []string{"mesh-addressing"}},
|
||||
Manifest{Module: "tailscale", Provides: []string{"private-network"}},
|
||||
), []string{"mesh-names", "tailscale"}, workstation(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("the mesh's names were installed over a VPN whose addresses it does not hand out")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mesh-addressing") {
|
||||
t.Fatalf("the refusal does not say what is missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARequirementWantedTwiceIsReportedOnce(t *testing.T) {
|
||||
// Two identical lines make a person hunt for the difference between them before realising
|
||||
// there is none.
|
||||
_, err := Resolve(shelf(
|
||||
Manifest{Module: "one", Requires: []string{"shell"}},
|
||||
Manifest{Module: "two", Requires: []string{"shell"}},
|
||||
Manifest{Module: "bash", Provides: []string{"shell"}},
|
||||
Manifest{Module: "zsh", Provides: []string{"shell"}},
|
||||
), []string{"one", "two"}, workstation(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("two shells and one was picked silently")
|
||||
}
|
||||
if n := strings.Count(err.Error(), `"shell" is wanted by`); n != 1 {
|
||||
t.Fatalf("the same requirement was reported %d times:\n%v", n, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user