package catalogue import ( "fmt" "sort" "strings" ) // The seats a mesh can have (novox/hq ADR 0110). // // **A closed set, defined here rather than by whoever claims one.** Until this, a well-formed name // became a seat by being claimed, so nothing could say which seats a mesh has or who fills them: // `the-showcase` and `the-build-machine` were each invented by the module claiming it. The set is // what a person reads to learn what a mesh can have, so an entry nobody argued for is an entry // nobody can explain — the same reason every shape in the host's vocabulary names its decision. // // A seat is held by a module assignment. What the mesh knows about a holder is what it knows about // that assignment; nothing about holders is kept here or anywhere else. // Seat is one role the mesh defines. type Seat struct { // Name is what a manifest claims. Name string // Scope is where there may be only one holder. Scope string // Delivers is the provision the seat's holder answers for, or empty. A seat that delivers a // provision may only be held by a module providing it at the seat's scope, and its holder is // what a requirement for that provision resolves to when several modules provide it. Delivers string // Accepts, Emits and Serves are the protocol of the role, as local verbs — the same three a // module declares for a seat of its own (novox/hq ADR 0118), and empty for most of these: a seat // is usually about who does a job and not about what may be said to them. // // **Named here so the mesh has no role it cannot describe** (ADR 0121). Without them a build // machine had three audiences for one outcome and nothing derived a grant for any of them, and an // event about a role had nowhere to live but the namespace of whichever module held that role // today — which the bus refuses, because a namespace belongs to who it is named for. Accepts []string Emits []string Serves []string // Decision is the record that made it a seat. Decision string } // 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"}, // **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: "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 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 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 } // SeatDelivering is the seat whose holder answers for a provision, if there is one. func SeatDelivering(provision string) (Seat, bool) { if provision == "" { return Seat{}, false } for _, s := range seats { if s.Delivers == provision { return s, true } } return Seat{}, false } // claimProblems is what is wrong with a manifest's claims and the seats it defines. // // 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 _, known := SeatNamed(c.Name); known { // **A seat's scope and what it delivers are not judged here** (novox/hq ADR 0122). // This function runs wherever a manifest is parsed, and one of those places is the // build machine, which has no store: there, `SeatNamed` answers from the set the // binary shipped with, so a build would be refused for disagreeing with a compiled // copy of data the control plane owns. Exactly that happened — a holder of the bus // seat was refused for not providing what a stale compiled row said the seat // delivered, while the store's own row said otherwise. // // Both checks moved to CatalogueProblems, which only ever runs in the control plane, // after UseSeats has replaced the set with the store's. continue } 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 } 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 c.At() != d.At() { problems = append(problems, fmt.Sprintf( "%s claims its own seat %s at scope %q, having declared it at %q", m.Module, c.Name, c.At(), d.At())) } } return problems } // CanHold is why a module could not hold a seat, or nothing: its definition must claim the seat at // the seat's scope, and provide what the seat delivers, if it delivers anything. The seat is the // store's row, so this is judged only where the store's set is loaded — at registration and in the // handover command (novox/hq ADR 0131), never in the parser. func CanHold(m Manifest, seat Seat) error { var claimed *Claim for i := range m.Claims { if hs, ok := SeatNamed(m.Claims[i].Name); ok && hs.Name == seat.Name { claimed = &m.Claims[i] } } if claimed == nil { return fmt.Errorf("%s does not claim %s", m.Module, seat.Name) } if claimed.At() != seat.Scope { return fmt.Errorf("%s claims %s at scope %q, and %s is a %s seat", m.Module, seat.Name, claimed.At(), seat.Name, seat.Scope) } if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) { return fmt.Errorf("%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope", m.Module, seat.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope) } return nil } func providesAt(m Manifest, provision, scope string) bool { for _, o := range m.Provides { if o.Name == provision && o.At() == scope { return true } } return false } func seatNames() string { names := make([]string, 0, len(seats)) for _, s := range seats { names = append(names, s.Name) } sort.Strings(names) return strings.Join(names, ", ") } // HolderAmong is which of several providers of a provision holds the seat that delivers it. // // Found by the (node, module) pair, because a provider is identified by both (novox/hq to-be 23): // two modules on one node could both provide a provision, and only the one holding the seat // answers for it. Nothing when no seat delivers the provision, when nobody holds // it, or when the holder is not among the providers offered. func HolderAmong(provision string, providers []Provider, held []Held) (Provider, bool) { seat, delivered := SeatDelivering(provision) if !delivered { return Provider{}, false } for _, h := range held { // 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 { if p.Node == h.Node && p.Module == h.Module { return p, true } } } return Provider{}, false } // 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. // // Most of the set is not here, and that is the ordinary case: a seat saying only who does a job grants // nothing on the bus and needs no queue. func SeatsWithAProtocol() []Seat { var out []Seat for _, s := range seats { if len(s.Accepts) > 0 || len(s.Emits) > 0 || len(s.Serves) > 0 { out = append(out, s) } } return out }