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.
200 lines
8.7 KiB
Go
200 lines
8.7 KiB
Go
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
|
|
// Decision is the record that made it a seat.
|
|
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{
|
|
{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.
|
|
{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"},
|
|
{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.
|
|
func Seats() []Seat {
|
|
return append([]Seat(nil), seats...)
|
|
}
|
|
|
|
// SeatNamed is the seat a claim names, if the mesh defines one.
|
|
func SeatNamed(name string) (Seat, bool) {
|
|
for _, s := range seats {
|
|
if s.Name == name {
|
|
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 against the set.
|
|
//
|
|
// 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.
|
|
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
|
|
// 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.
|
|
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))
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
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 {
|
|
if h.Claim != 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
|
|
}
|
|
|
|
// 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",
|
|
}
|