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
@@ -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) {