ADR 0121, first half. The `mesh-*` seats said who does a job and nothing about what may be said to them or by them, so the mesh had roles it could not describe. They take the same three fields a module's seat has now, and the machinery that already derives a work queue, a holder's worker and a permission set from a declared seat does it for these too. The build-machine role accepts a build and emits an outcome, so `mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone. A work queue shared by several build machines is what a seat's `accepts` already is, and keeping a second mechanism for it was two places a permission could be wrong. The controller's own side of a seat is a named list rather than something derived: it is not a module and declares no `uses`, so which roles the mesh itself submits work to has to be stated — and stating it makes that question answerable. Two things this caught: **The followed event subjects were hard-coded and had just gone stale.** They were written out while the catalogue still spelled its events as the old bus's routing keys, so converting those (issue 127) turned the pair into a controller listening to a subject nothing publishes — the same fault as the issue, from the other side. They derive from the emitter and the event name now, through the same function the permission uses, so the two cannot drift apart. **A role's queue exists before its holder**, checked against a real server, and asserting twice changes nothing. Work queues until somebody arrives to do it, so assigning a build machine later flushes the backlog instead of having lost it.
231 lines
10 KiB
Go
231 lines
10 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
|
|
// 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
|
|
}
|
|
|
|
// 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"},
|
|
// A build is work submitted to this role, and its outcome is the role's own event (ADR 0121).
|
|
// 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"},
|
|
}
|
|
|
|
// 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",
|
|
}
|
|
|
|
// 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
|
|
}
|