Rename the mesh's seats to mesh-*, keeping their interfaces

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.
This commit is contained in:
2026-09-26 23:07:09 +02:00
parent aa74bd86ca
commit 173c8c7c21
11 changed files with 114 additions and 40 deletions
+4 -4
View File
@@ -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" {
+1 -1
View File
@@ -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 {
+9 -9
View File
@@ -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)
}
}
@@ -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)
}
}
+39
View File
@@ -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 <nil>", 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)
}
}
+5 -5
View File
@@ -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)
}
}
@@ -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)
}
@@ -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)
}
}
+46 -11
View File
@@ -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",
}
+4 -4
View File
@@ -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) {
+1 -1
View File
@@ -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`