diff --git a/cmd/mesh-controller/seats_test.go b/cmd/mesh-controller/seats_test.go index 469d3a3..dc8d258 100644 --- a/cmd/mesh-controller/seats_test.go +++ b/cmd/mesh-controller/seats_test.go @@ -35,13 +35,13 @@ func TestEverySeatIsListedIncludingTheOnesNobodyHolds(t *testing.T) { func TestANodeSeatListsEveryMachineHoldingIt(t *testing.T) { rows, _ := seatsHeld(catalogue.Seats(), []catalogue.Held{ - {Claim: "the-packet-filter", Scope: catalogue.ScopeNode, Node: "node2", Module: "nftables"}, - {Claim: "the-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"}, + {Claim: "mesh-packet-filter", Scope: catalogue.ScopeNode, Node: "node2", Module: "nftables"}, + {Claim: "mesh-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"}, // Resolved twice, reported once: a machine is one holder however many passes saw it. - {Claim: "the-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"}, + {Claim: "mesh-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"}, }) for _, r := range rows { - if r.Seat != "the-packet-filter" { + if r.Seat != "mesh-packet-filter" { continue } if len(r.Holders) != 2 || r.Holders[0].Node != "anchor" || r.Holders[1].Node != "node2" { diff --git a/cmd/mesh-controller/source.go b/cmd/mesh-controller/source.go index 760dda7..292db22 100644 --- a/cmd/mesh-controller/source.go +++ b/cmd/mesh-controller/source.go @@ -18,7 +18,7 @@ import ( // seat's holder runs. // gitSeat is the seat a self-hosted repository lives on. -const gitSeat = "git" +const gitSeat = "mesh-git" // buildSource is where a build's repository is: a URL, or a path on a seat's holder. type buildSource struct { diff --git a/cmd/mesh-controller/source_test.go b/cmd/mesh-controller/source_test.go index cd0030b..9602471 100644 --- a/cmd/mesh-controller/source_test.go +++ b/cmd/mesh-controller/source_test.go @@ -11,7 +11,7 @@ import ( func forgeHolding(port any) catalogue.World { return catalogue.World{ - Held: []catalogue.Held{{Claim: "git", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "gitea"}}, + Held: []catalogue.Held{{Claim: "mesh-git", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "gitea"}}, Offered: map[string][]catalogue.Provider{"git": { // A second forge that does not hold the seat, so taking the first one found would be wrong. {Node: "archive", At: "archive.internal", Module: "gitea-mirror", @@ -23,7 +23,7 @@ func forgeHolding(port any) catalogue.World { } func TestARepositoryOnTheSeatIsClonedFromItsHolder(t *testing.T) { - got, err := clonedFromSeat(forgeHolding(float64(3000)), "git", "novox/mesh-catalog") + got, err := clonedFromSeat(forgeHolding(float64(3000)), "mesh-git", "novox/mesh-catalog") if err != nil { t.Fatal(err) } @@ -35,7 +35,7 @@ func TestARepositoryOnTheSeatIsClonedFromItsHolder(t *testing.T) { func TestAMovedForgeIsFollowedWithoutRewritingAnything(t *testing.T) { // The whole point: the node gave the forge another port, and the same recorded path clones // from the new one. Nothing recorded contained the old one to be wrong. - got, err := clonedFromSeat(forgeHolding(float64(3100)), "git", "novox/mesh-catalog") + got, err := clonedFromSeat(forgeHolding(float64(3100)), "mesh-git", "novox/mesh-catalog") if err != nil { t.Fatal(err) } @@ -45,11 +45,11 @@ func TestAMovedForgeIsFollowedWithoutRewritingAnything(t *testing.T) { } func TestWithNobodyHoldingTheSeatASelfHostedBuildIsRefusedAndSaysWhy(t *testing.T) { - _, err := clonedFromSeat(catalogue.World{}, "git", "novox/mesh-catalog") + _, err := clonedFromSeat(catalogue.World{}, "mesh-git", "novox/mesh-catalog") if err == nil { t.Fatal("a repository was cloned from a forge the mesh does not have") } - for _, want := range []string{"nobody holds the git seat", "without --self"} { + for _, want := range []string{"nobody holds the mesh-git seat", "without --self"} { if !strings.Contains(err.Error(), want) { t.Fatalf("the refusal does not say %q: %v", want, err) } @@ -71,7 +71,7 @@ func TestAnExternalRepositoryIsClonedExactlyAsGiven(t *testing.T) { func TestAHolderOffThePrivateNetworkIsRefused(t *testing.T) { world := forgeHolding(float64(3000)) world.Offered["git"][1].At = "" - if _, err := clonedFromSeat(world, "git", "novox/mesh-catalog"); err == nil || + if _, err := clonedFromSeat(world, "mesh-git", "novox/mesh-catalog"); err == nil || !strings.Contains(err.Error(), "private network") { t.Fatalf("a forge nothing can reach was cloned from: %v", err) } @@ -79,7 +79,7 @@ func TestAHolderOffThePrivateNetworkIsRefused(t *testing.T) { func TestAHolderServingNoPortIsRefusedRatherThanGuessed(t *testing.T) { // A default port would be the forge's address guessed, which is what this exists to stop. - if _, err := clonedFromSeat(forgeHolding(nil), "git", "novox/mesh-catalog"); err == nil { + if _, err := clonedFromSeat(forgeHolding(nil), "mesh-git", "novox/mesh-catalog"); err == nil { t.Fatal("a port was guessed for a forge that serves none") } } @@ -101,8 +101,8 @@ func TestAnAddressGivenAsAPathOnTheForgeIsRefused(t *testing.T) { } func TestASourceOnTheSeatReadsAsAPathNotAnAddress(t *testing.T) { - s := buildSource{Repository: "novox/mesh-catalog", Seat: "git"} - if got := s.String(); got != "novox/mesh-catalog on the git seat" { + s := buildSource{Repository: "novox/mesh-catalog", Seat: "mesh-git"} + if got := s.String(); got != "novox/mesh-catalog on the mesh-git seat" { t.Fatalf("read as %q", got) } } diff --git a/internal/catalogue/artifact_store_seat_test.go b/internal/catalogue/artifact_store_seat_test.go index 4b658e4..182dcac 100644 --- a/internal/catalogue/artifact_store_seat_test.go +++ b/internal/catalogue/artifact_store_seat_test.go @@ -23,7 +23,7 @@ func TestASecondArtifactStoreAnywhereIsRefusedByName(t *testing.T) { } // A second one, on any other machine, is refused — and the refusal names the seat. - elsewhere := World{Held: []Held{{Claim: "the-artifact-store", Scope: ScopeMesh, + elsewhere := World{Held: []Held{{Claim: "mesh-artifact-store", Scope: ScopeMesh, Node: "anchor", Module: "distribution"}}} other := workstation() other.Name = "laptop" @@ -32,7 +32,7 @@ func TestASecondArtifactStoreAnywhereIsRefusedByName(t *testing.T) { t.Fatal("a second store was accepted on another machine; it would offer artifact-store a " + "second time and every consumer elsewhere would refuse to choose") } - if !strings.Contains(err.Error(), "the-artifact-store") || !strings.Contains(err.Error(), "one per mesh") { + if !strings.Contains(err.Error(), "mesh-artifact-store") || !strings.Contains(err.Error(), "one per mesh") { t.Fatalf("refused without naming the seat: %v", err) } } diff --git a/internal/catalogue/broker_seat_test.go b/internal/catalogue/broker_seat_test.go index 13ca8b3..a99bc71 100644 --- a/internal/catalogue/broker_seat_test.go +++ b/internal/catalogue/broker_seat_test.go @@ -63,3 +63,42 @@ func TestTheAmqpBrokerDoesNotContendForTheSeat(t *testing.T) { t.Fatalf("the amqp broker was refused beside the mesh bus: %v", err) } } + +// **A seat and the interface it delivers are different names, and renaming one must not rename +// the other** (novox/hq ADR 0118). This nearly went wrong: the seats were renamed to the `mesh-*` +// prefix, and a blanket search-and-replace also renamed `npm-package-registry` and `git` where +// they are *provisions* — which a consumer requires and a provider offers. The tests failed with +// "the package registry is served on ", which does not say "you renamed an interface". +func TestRenamingASeatDidNotRenameTheInterfaceItDelivers(t *testing.T) { + for _, pair := range []struct{ seat, delivers string }{ + {"mesh-git", "git"}, + {"mesh-npm-package-registry", "npm-package-registry"}, + {"mesh-artifact-store", "artifact-store"}, + {"mesh-store", "postgres-database"}, + {"mesh-broker", "mesh-bus"}, + } { + s, known := SeatNamed(pair.seat) + if !known { + t.Fatalf("%q is not a seat", pair.seat) + } + if s.Delivers != pair.delivers { + t.Errorf("the %s seat delivers %q, expected %q — renaming the seat moved the "+ + "interface with it, and every consumer requiring it would stop resolving", + pair.seat, s.Delivers, pair.delivers) + } + if _, isSeat := SeatNamed(pair.delivers); isSeat { + t.Errorf("%q is both an interface and a seat name; one of the renames was incomplete", + pair.delivers) + } + } +} + +// And a manifest written against an old seat name is told what it became, rather than refused as +// unknown — the courtesy the `needs`/`own-secrets` rename already sets. +func TestAnOldSeatNameSaysWhatItBecame(t *testing.T) { + m := Manifest{Module: "old", Claims: []Claim{{Name: "the-catalogue", Scope: ScopeMesh}}} + got := strings.Join(claimProblems(m), "; ") + if !strings.Contains(got, "the-catalogue") || !strings.Contains(got, "mesh-catalog") { + t.Fatalf("the refusal does not name both the old and the new: %q", got) + } +} diff --git a/internal/catalogue/domain_test.go b/internal/catalogue/domain_test.go index 967985b..299543b 100644 --- a/internal/catalogue/domain_test.go +++ b/internal/catalogue/domain_test.go @@ -17,7 +17,7 @@ func networkingShelf(extra ...Manifest) map[string]Manifest { {Module: "networking", Requires: []string{"private-network", "name-resolution"}}, {Module: "mesh-wireguard", Computed: "mesh-wireguard", Provides: Offers("private-network", "mesh-addressing"), - Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, + Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}}, {Module: "mesh-names", Computed: "mesh-names", Provides: Offers("name-resolution"), Requires: []string{"mesh-addressing"}}, } @@ -44,7 +44,7 @@ func TestASecondVPNTurnsItIntoAChoice(t *testing.T) { // back under another name. _, err := Resolve(networkingShelf( Manifest{Module: "tailscale", Provides: Offers("private-network"), - Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, + Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}}, ), []string{"networking"}, workstation(), World{}) if err == nil { @@ -62,7 +62,7 @@ func TestChoosingIsAssigning(t *testing.T) { got, err := Resolve(networkingShelf( Manifest{Module: "tailscale", Provides: Offers("private-network", "name-resolution"), - Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, + Claims: []Claim{{Name: "mesh-private-network", Scope: ScopeNode}}}, ), []string{"networking", "tailscale"}, workstation(), World{}) if err != nil { @@ -85,13 +85,13 @@ func TestChoosingOneVPNCannotDragTheOtherBackIn(t *testing.T) { // 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: "the-private-network", Scope: ScopeNode}}}, + 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(), "the-private-network") { + if !strings.Contains(err.Error(), "mesh-private-network") { t.Fatalf("the refusal does not say what collided: %v", err) } } diff --git a/internal/catalogue/foundation_manifests_test.go b/internal/catalogue/foundation_manifests_test.go index 5565993..10661bf 100644 --- a/internal/catalogue/foundation_manifests_test.go +++ b/internal/catalogue/foundation_manifests_test.go @@ -124,7 +124,7 @@ func TestTheForgesPortIsGivenLikeAnyOtherProvidersPort(t *testing.T) { // unused. func TestTheBuilderRequiresTheRegistryTheNpmSeatDelivers(t *testing.T) { builder := catalogueManifest(t, "builder") - seat, _ := SeatNamed("npm-package-registry") + seat, _ := SeatNamed("mesh-npm-package-registry") var requires bool for _, r := range builder.Requires { requires = requires || r == seat.Delivers @@ -150,7 +150,7 @@ func TestTheForgeHoldsTheNpmAndGitSeats(t *testing.T) { for _, c := range forge.Claims { holds[c.Name] = true } - for _, seat := range []string{"npm-package-registry", "git"} { + for _, seat := range []string{"mesh-npm-package-registry", "mesh-git"} { if !holds[seat] { t.Errorf("gitea does not claim the %s seat: %+v", seat, forge.Claims) } diff --git a/internal/catalogue/resolver_manifests_test.go b/internal/catalogue/resolver_manifests_test.go index 3336380..baccc94 100644 --- a/internal/catalogue/resolver_manifests_test.go +++ b/internal/catalogue/resolver_manifests_test.go @@ -170,7 +170,7 @@ func TestTwoThingsDecidingWhatAMachineAsksAreRefused(t *testing.T) { if err == nil { t.Fatal("resolv-conf and resolved-split-dns were both assigned to one machine") } - if !strings.Contains(err.Error(), "the-resolver-configuration") { + if !strings.Contains(err.Error(), "mesh-resolver-configuration") { t.Fatalf("the refusal does not say what was claimed: %v", err) } } diff --git a/internal/catalogue/seats.go b/internal/catalogue/seats.go index 6305ec3..bfb3f7e 100644 --- a/internal/catalogue/seats.go +++ b/internal/catalogue/seats.go @@ -49,17 +49,17 @@ var seats = []Seat{ // `mesh-bus` is the mesh's own; `nats` is a private NATS server a module may provide as a // backing service, the way `amqp` is provided (ADR 0119). Never the same name. {Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "novox/hq ADR 0079"}, - {Name: "the-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"}, - {Name: "the-catalogue", Scope: ScopeMesh, Decision: "novox/hq ADR 0110"}, - {Name: "npm-package-registry", Scope: ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"}, - {Name: "git", Scope: ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0111"}, - {Name: "the-build-machine", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, - {Name: "the-dns-port", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, - {Name: "the-intrusion-prevention", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, - {Name: "the-packet-filter", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, - {Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, - {Name: "the-resolver-configuration", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, - {Name: "the-showcase", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"}, + {Name: "mesh-catalog", Scope: ScopeMesh, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-npm-package-registry", Scope: ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"}, + {Name: "mesh-git", Scope: ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0111"}, + {Name: "mesh-build-machine", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-dns-port", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-intrusion-prevention", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-packet-filter", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-resolver-configuration", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, + {Name: "mesh-showcase", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, } // Seats is every seat the mesh defines, in reading order. @@ -98,6 +98,16 @@ func SeatDelivering(provision string) (Seat, bool) { func claimProblems(m Manifest) []string { var problems []string for _, c := range m.Claims { + if now, was := renamedSeats[c.Name]; was { + // Named rather than refused as unknown: whoever wrote it knew what they meant, and + // the mesh knows what it is called now — the same courtesy the `needs`/`own-secrets` + // rename gets. Without this the refusal would be "not a seat", which sends somebody + // reading code for a name that is one character different. + problems = append(problems, fmt.Sprintf( + "%s claims %q, which is now called %q (novox/hq ADR 0118: the mesh's own seats "+ + "are named mesh-*, and the prefix is what reserves them)", m.Module, c.Name, now)) + continue + } seat, known := SeatNamed(c.Name) if !known { // Not one of the mesh's own, which no longer means it is not a seat: a module may @@ -162,3 +172,28 @@ func HolderAmong(provision string, providers []Provider, held []Held) (Provider, } return Provider{}, false } + +// renamedSeats is what the mesh's own seats used to be called (novox/hq ADR 0118). +// +// **A rename here is not a data migration**, which ADR 0118 assumed it was and a progressive +// insight there corrects: a seat's holding is *derived* at resolution from the claims in +// manifests (`resolve.go`), never stored, so there are no recorded old names to rewrite. What +// exists is source — manifests in the catalogue — and this list is how one written against the +// old name is told what it became rather than refused as unknown. +// +// It is kept, not retired after the catalogue is updated: a module lives in its own repository +// ([ADR 0069]) and may be registered from anywhere, so an old name can arrive long after the +// catalogue beside this checkout stopped using one. +var renamedSeats = map[string]string{ + "the-artifact-store": "mesh-artifact-store", + "the-catalogue": "mesh-catalog", + "npm-package-registry": "mesh-npm-package-registry", + "git": "mesh-git", + "the-build-machine": "mesh-build-machine", + "the-dns-port": "mesh-dns-port", + "the-intrusion-prevention": "mesh-intrusion-prevention", + "the-packet-filter": "mesh-packet-filter", + "the-private-network": "mesh-private-network", + "the-resolver-configuration": "mesh-resolver-configuration", + "the-showcase": "mesh-showcase", +} diff --git a/internal/catalogue/seats_test.go b/internal/catalogue/seats_test.go index 3555c8c..bace194 100644 --- a/internal/catalogue/seats_test.go +++ b/internal/catalogue/seats_test.go @@ -92,7 +92,7 @@ func TestAClaimOnASeatAnotherModuleDeclaresIsAccepted(t *testing.T) { } func TestASeatClaimedAtAnotherScopeIsRefused(t *testing.T) { - _, err := ParseManifest(claimed(`[{"name":"npm-package-registry","scope":"node"}]`)) + _, err := ParseManifest(claimed(`[{"name":"mesh-npm-package-registry","scope":"node"}]`)) if err == nil { t.Fatal("a mesh seat was held per node") } @@ -104,7 +104,7 @@ func TestASeatClaimedAtAnotherScopeIsRefused(t *testing.T) { func TestADeliveringSeatIsOnlyHeldByAModuleThatProvides(t *testing.T) { // Holding it makes the module the mesh's answer for the provision. A module that cannot answer // would be the answer anyway, and every consumer would be sent to it. - raw := []byte(`{"module":"thing","version":"1","claims":[{"name":"git","scope":"mesh"}]}`) + raw := []byte(`{"module":"thing","version":"1","claims":[{"name":"mesh-git","scope":"mesh"}]}`) _, err := ParseManifest(raw) if err == nil { t.Fatal("a module holding the git seat need not provide git") @@ -163,7 +163,7 @@ func TestEveryManifestInUseClaimsASeatTheMeshDefines(t *testing.T) { func registryShelf() map[string]Manifest { return shelf( Manifest{Module: "gitea", Version: "1", Provides: FromAnywhere("npm-package-registry"), - Claims: []Claim{{Name: "npm-package-registry", Scope: ScopeMesh}}}, + Claims: []Claim{{Name: "mesh-npm-package-registry", Scope: ScopeMesh}}}, Manifest{Module: "verdaccio", Version: "1", Provides: FromAnywhere("npm-package-registry")}, Manifest{Module: "builder", Version: "1", Requires: []string{"npm-package-registry"}}, ) @@ -177,7 +177,7 @@ func twoRegistries() map[string][]Provider { } func giteaHoldsTheSeat() []Held { - return []Held{{Claim: "npm-package-registry", Scope: ScopeMesh, Node: "anchor", Module: "gitea"}} + return []Held{{Claim: "mesh-npm-package-registry", Scope: ScopeMesh, Node: "anchor", Module: "gitea"}} } func TestTheSeatsHolderAnswersWhenSeveralProvide(t *testing.T) { diff --git a/internal/overlay/generator.go b/internal/overlay/generator.go index eb0b9c9..4eb054a 100644 --- a/internal/overlay/generator.go +++ b/internal/overlay/generator.go @@ -52,7 +52,7 @@ const Addressing = "mesh-addressing" // for two different purposes. Being **the** one the mesh runs over is singular, and without // saying so a person who chose a different VPN can still end up with this one dragged back in by // something that needed the mesh's own addresses. Which is exactly what happened, once. -const TheNetwork = "the-private-network" +const TheNetwork = "mesh-private-network" // **A resolver's data went the same way as the names.** Two constants used to sit here — a // `mesh-resolver` module that would write the machines as wildcards, and a `resolver-data`