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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user