novox/hq ADR 0118: the prefix is the reservation rule, so a module declaring any mesh-* name is refused and there is no reserved-names list to drift. Ten seats renamed in the table, the manifests that claim them, the controller's own shipped manifests, and the tests. Not the migration 0118 expected: a holding is derived at resolution from manifests and never stored, so nothing recorded points at an old name. A kept rename table tells a manifest written against one what it became — kept rather than retired, because a module lives in its own repository and may be registered long after the catalogue stopped using it. **A seat is not the interface it delivers.** The git seat became mesh-git and the git provision did not; likewise the package registry. A blanket replace renamed both, and the failure read "the package registry is served on <nil>", which does not say "you renamed an interface". A test now pins every seat against what it delivers, and that neither name is also the other.
135 lines
5.3 KiB
Go
135 lines
5.3 KiB
Go
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: Offers("private-network", "mesh-addressing"),
|
|
Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}},
|
|
{Module: "mesh-names", Computed: "mesh-names",
|
|
Provides: Offers("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(), World{})
|
|
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: Offers("private-network"),
|
|
Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}},
|
|
), []string{"networking"}, workstation(), World{})
|
|
|
|
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: Offers("private-network", "name-resolution"),
|
|
Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}},
|
|
), []string{"networking", "tailscale"}, workstation(), World{})
|
|
|
|
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: Offers("private-network"),
|
|
Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}},
|
|
), []string{"networking", "tailscale"}, workstation(), World{})
|
|
|
|
if err == nil {
|
|
t.Fatal("a machine was given two private networks without being told")
|
|
}
|
|
if !strings.Contains(err.Error(), "mesh-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: Offers("name-resolution"), Requires: []string{"mesh-addressing"}},
|
|
Manifest{Module: "tailscale", Provides: Offers("private-network")},
|
|
), []string{"mesh-names", "tailscale"}, workstation(), World{})
|
|
|
|
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: Offers("shell")},
|
|
Manifest{Module: "zsh", Provides: Offers("shell")},
|
|
), []string{"one", "two"}, workstation(), World{})
|
|
|
|
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)
|
|
}
|
|
}
|