Merge main: the trunk renamed the seats and made them data
Both branches changed the seat set from the same starting point, so every number collided and every `mesh-*` name existed twice. The trunk's numbers and names win: this branch's records became 0129/0130 and its migrations 0037/0038, and the hardcoded rename map gave way to the trunk's `seat_alias` table — a rename is a row now (ADR 0122), not a recompile. Three of my checks were wrong and the merge is what showed it: A seat with an empty protocol is a marker, not an incomplete declaration. Most node-scoped seats are markers — which module is this machine's packet filter — and refusing one refused most of the set, the showcase module included. A mistyped field name is already refused by the parser, so an empty protocol was written as one deliberately. A claim on a seat this manifest does not declare is not the parser's to judge. A module may hold a seat another module declared; that is the whole reason ADR 0126 has callers name the seat and not its provider. Whether the seat exists is a fact about the catalogue, so the refusal is at registration, where every declaration is in view. And a seat may share a name with the provision it delivers. `git`, the npm registry and the artifact store still do, because renaming a delivering seat cascades to every consumer requiring it, with a window where a holder stops resolving mid-flight. The trunk deferred exactly those three on purpose. Full suite green against a real NATS and store.
This commit is contained in:
+136
-83
@@ -42,54 +42,108 @@ type Seat struct {
|
||||
Decision string
|
||||
}
|
||||
|
||||
// seats is the whole set, in the order a person reads it: the mesh's own, then a node's.
|
||||
var seats = []Seat{
|
||||
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
|
||||
// fallback when it has none (novox/hq ADR 0122). It is the one place the closed set 0110 defines is
|
||||
// written; the store's table is seeded from it and thereafter is the live, editable copy.
|
||||
//
|
||||
// In the order a person reads it: the mesh's own, then a node's.
|
||||
var defaultSeats = []Seat{
|
||||
{Name: "mesh-controller", Scope: ScopeMesh, Decision: "novox/hq ADR 0079"},
|
||||
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079"},
|
||||
// What holding this delivers is the mesh's own bus (novox/hq ADR 0120): a module that speaks
|
||||
// to the mesh requires `mesh-bus` and receives an address, a sealed credential and the trust
|
||||
// to verify the server. A module that requires nothing gets no account at all — 23 of the
|
||||
// catalogue's 72 never speak, and an ambient connection would mint a credential for each.
|
||||
//
|
||||
// Corrected twice in one day, which is worth the comment. It read `amqp`, which was the old
|
||||
// broker's interface and not this seat's; ADR 0117 emptied it, reasoning that a bus cannot be
|
||||
// provisioned; and it is neither. The bus's accounts are composed by the controller rather
|
||||
// than created by a provisioner, so nothing waits on a bus account in order to make one —
|
||||
// which is a fact about the *mechanism*, not a reason the connection cannot be required.
|
||||
//
|
||||
// `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.
|
||||
// **Delivers the mesh's own bus, not `amqp`.** Those were the same word until
|
||||
// ADR 0127 separated them: `amqp` is a backing service a module may require, and this seat is
|
||||
// the mesh's own transport. ADR 0128 then made that connection something a module requires
|
||||
// rather than receives ambiently — 23 of the catalogue's modules never speak, and an ambient
|
||||
// connection would mint a credential for each.
|
||||
{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "novox/hq ADR 0079"},
|
||||
{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"},
|
||||
// A build is work submitted to this role, and its outcome is the role's own event (ADR 0121).
|
||||
{Name: "the-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"},
|
||||
{Name: "mesh-catalog", Scope: ScopeMesh, Decision: "novox/hq ADR 0121"},
|
||||
// Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a
|
||||
// delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight.
|
||||
// They keep their names until that migration is done deliberately, apart from the node-* pass.
|
||||
{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"},
|
||||
// A build is work submitted to this role and its outcome is the role's own event (ADR 0129).
|
||||
// One publish reaches whoever asked, the controller that records it, and the catalogue that
|
||||
// places it in the module graph — which is what the old bus's shared exchange did for free, and
|
||||
// what a dedicated build branch was doing a second way.
|
||||
{Name: "mesh-build-machine", Scope: ScopeNode,
|
||||
Accepts: []string{"build"}, Emits: []string{"built"}, 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"},
|
||||
// places it in the graph — what the old bus's shared exchange did for free.
|
||||
{Name: "mesh-build-machine", Scope: ScopeMesh,
|
||||
Accepts: []string{"build"}, Emits: []string{"built"}, Decision: "novox/hq ADR 0121"},
|
||||
{Name: "node-dns-resolver", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
|
||||
{Name: "node-intrusion-prevention", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
|
||||
{Name: "node-packet-filter", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
|
||||
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
|
||||
// model change, not a rename, so it stays until that is built.
|
||||
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
|
||||
{Name: "node-resolver-config", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
|
||||
// The program that manages the machine's own network. It delivers nothing: its holder only
|
||||
// keeps the manager and the mesh from contradicting each other — the resolver file left to the
|
||||
// mesh, the private network's interface left alone — and never declares a link, an address or
|
||||
// a wireless network, because the link is the only channel a fix could arrive on. A seat
|
||||
// rather than a condition in the resolver's module, so a machine running two managers is
|
||||
// refused at assignment instead of found by the resolver being rewritten (novox/hq ADR 0117).
|
||||
{Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117"},
|
||||
}
|
||||
|
||||
// A system seat name is the control plane's namespace: `mesh-*` for a mesh-wide role, `node-*` for
|
||||
// a per-node one (novox/hq ADR 0121). A claim to a system name the mesh does not define is refused;
|
||||
// any other name is a module's own to define and claim. Some of the mesh's own seats predate this
|
||||
// convention and are not yet renamed (git, npm-package-registry, the-artifact-store,
|
||||
// the-private-network) — those are in the set, so they resolve by name, not by prefix.
|
||||
func isSystemSeatName(name string) bool {
|
||||
return strings.HasPrefix(name, "mesh-") || strings.HasPrefix(name, "node-")
|
||||
}
|
||||
|
||||
// seats is the working set the lookups read. It starts as the compiled defaults and is replaced by
|
||||
// what the control plane loaded from its store (novox/hq ADR 0122), so a change to the set is a
|
||||
// change to data, not to this code.
|
||||
var seats = defaultSeats
|
||||
|
||||
// DefaultSeats is the set the mesh ships with, for seeding the store's seat table.
|
||||
func DefaultSeats() []Seat { return append([]Seat(nil), defaultSeats...) }
|
||||
|
||||
// UseSeats replaces the working set with the one the control plane read from its store.
|
||||
//
|
||||
// **Empty is ignored on purpose.** A store that has not been seeded yet — or one that could not be
|
||||
// read — must leave the compiled defaults in force rather than emptying the set: an empty set would
|
||||
// refuse every claim and could stop the control plane composing at all, which is a far worse failure
|
||||
// than running on the set the binary shipped with. So the store can only ever *replace* the set with
|
||||
// a non-empty one, never erase it.
|
||||
func UseSeats(s []Seat) {
|
||||
if len(s) > 0 {
|
||||
seats = s
|
||||
}
|
||||
}
|
||||
|
||||
// aliases maps a seat's former names to its current canonical name (novox/hq ADR 0122). Loaded from
|
||||
// the store alongside the set, so a reference to a name a seat used to have — a manifest's claim, a
|
||||
// held record — still resolves to it after a rename, and nothing downstream has to change.
|
||||
var aliases = map[string]string{}
|
||||
|
||||
// UseAliases replaces the former-name map with the one the control plane read from its store. Empty
|
||||
// is fine and ordinary: a mesh whose seats have never been renamed has no aliases.
|
||||
func UseAliases(m map[string]string) { aliases = m }
|
||||
|
||||
// Seats is every seat the mesh defines, in reading order.
|
||||
func Seats() []Seat {
|
||||
return append([]Seat(nil), seats...)
|
||||
}
|
||||
|
||||
// SeatNamed is the seat a claim names, if the mesh defines one.
|
||||
// SeatNamed is the seat a name refers to, whether that is its current name or one it used to have
|
||||
// (novox/hq ADR 0122). A former name resolves to the seat's canonical row, so a rename breaks no
|
||||
// reference to the old name.
|
||||
func SeatNamed(name string) (Seat, bool) {
|
||||
for _, s := range seats {
|
||||
if s.Name == name {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
if canonical, aliased := aliases[name]; aliased {
|
||||
for _, s := range seats {
|
||||
if s.Name == canonical {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return Seat{}, false
|
||||
}
|
||||
|
||||
@@ -106,42 +160,63 @@ func SeatDelivering(provision string) (Seat, bool) {
|
||||
return Seat{}, false
|
||||
}
|
||||
|
||||
// claimProblems is what is wrong with a manifest's claims against the set.
|
||||
// claimProblems is what is wrong with a manifest's claims and the seats it defines.
|
||||
//
|
||||
// Three refusals, each naming the seat: a seat the mesh does not define, a seat claimed at another
|
||||
// scope, and a seat that delivers a provision claimed by a module that does not provide it — which
|
||||
// would make the module the mesh's answer for something it cannot answer.
|
||||
// A claim is one of three things (novox/hq ADR 0121): a **system seat** the control plane defines —
|
||||
// checked for scope and, if it delivers a provision, that the claimant provides it; a **system name
|
||||
// the mesh does not define** (`mesh-*`/`node-*`) — refused, because that namespace is the control
|
||||
// plane's; or a **module-defined seat** — valid only when this manifest also declares it, since a
|
||||
// module may coordinate its own instances through a seat of its own but may not invent one by
|
||||
// claiming it. A module's own seat declaration may not sit in the system namespace or shadow a
|
||||
// system seat.
|
||||
func claimProblems(m Manifest) []string {
|
||||
var problems []string
|
||||
|
||||
defined := map[string]SeatDeclaration{}
|
||||
for _, d := range m.DefinesSeats {
|
||||
if _, isSystem := SeatNamed(d.Name); isSystem || isSystemSeatName(d.Name) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s defines a seat %q in the mesh's own namespace; a module's seat is named outside "+
|
||||
"mesh-*/node-* (novox/hq ADR 0121)", m.Module, d.Name))
|
||||
continue
|
||||
}
|
||||
defined[d.Name] = d
|
||||
}
|
||||
|
||||
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))
|
||||
if seat, known := SeatNamed(c.Name); known {
|
||||
if c.At() != seat.Scope {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s at scope %q, and %s is a %s seat",
|
||||
m.Module, c.Name, c.At(), c.Name, seat.Scope))
|
||||
}
|
||||
if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope",
|
||||
m.Module, c.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope))
|
||||
}
|
||||
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
|
||||
// declare its own (novox/hq ADR 0118), and whether anybody declared *this* one is a
|
||||
// fact about the catalogue rather than about this manifest. Deferred to
|
||||
// CatalogueProblems, which refuses it at registration — the same guarantee ADR 0110
|
||||
// wanted, at the same moment, from a set nobody maintains by hand.
|
||||
if isSystemSeatName(c.Name) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %q, which is a seat in the mesh's own namespace (mesh-*/node-*) that it "+
|
||||
"does not define (novox/hq ADR 0121) — the seats are: %s", m.Module, c.Name, seatNames()))
|
||||
continue
|
||||
}
|
||||
if c.At() != seat.Scope {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s at scope %q, and %s is a %s seat",
|
||||
m.Module, c.Name, c.At(), c.Name, seat.Scope))
|
||||
d, ours := defined[c.Name]
|
||||
if !ours {
|
||||
// **A claim on a seat this manifest does not declare is not the parser's to judge.**
|
||||
// A module may hold a seat another module declared — that is why ADR 0126 has callers
|
||||
// name the seat and not its provider, so an implementation can be replaced without
|
||||
// touching a caller. Whether the seat exists is a fact about the whole catalogue, so
|
||||
// the refusal is at registration, where every declaration is in view
|
||||
// (`CatalogueProblems`: "which no module declares and the mesh does not define").
|
||||
continue
|
||||
}
|
||||
if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) {
|
||||
if c.At() != d.At() {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope",
|
||||
m.Module, c.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope))
|
||||
"%s claims its own seat %s at scope %q, having declared it at %q",
|
||||
m.Module, c.Name, c.At(), d.At()))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
@@ -177,7 +252,10 @@ func HolderAmong(provision string, providers []Provider, held []Held) (Provider,
|
||||
return Provider{}, false
|
||||
}
|
||||
for _, h := range held {
|
||||
if h.Claim != seat.Name || h.Scope != seat.Scope {
|
||||
// Resolve the held claim to a seat rather than comparing names, so a record naming a seat's
|
||||
// former name still matches it after a rename (novox/hq ADR 0122).
|
||||
hs, ok := SeatNamed(h.Claim)
|
||||
if !ok || hs.Name != seat.Name || h.Scope != seat.Scope {
|
||||
continue
|
||||
}
|
||||
for _, p := range providers {
|
||||
@@ -189,31 +267,6 @@ 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",
|
||||
}
|
||||
|
||||
// SeatsWithAProtocol are the mesh's own seats that say something about what may be said to them or by
|
||||
// them, which is the set the bus derives streams, consumers and permissions from.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user