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:
@@ -206,10 +206,18 @@ type Manifest struct {
|
||||
// that every new module would force its predecessors to update.
|
||||
Claims []Claim `json:"claims,omitempty"`
|
||||
|
||||
// Seats this module declares of its own, with their protocols (novox/hq ADR 0118). The set
|
||||
// of seats a mesh has is the mesh's own plus these, derived from what is registered rather
|
||||
// than written in the controller — closed, and extensible without changing the mesh.
|
||||
Seats []SeatDeclaration `json:"seats,omitempty"`
|
||||
// DefinesSeats are the seats this module defines for itself, with their protocols
|
||||
// (novox/hq ADR 0121, ADR 0129). The control plane defines the system seats — `mesh-*` and
|
||||
// `node-*` — and a module may define its own, named outside that namespace, to coordinate its
|
||||
// own instances: the mesh enforces one-holder-per-scope for it without knowing what it means. A
|
||||
// module's declared seat is the only non-system name it may then claim; a claim to a name
|
||||
// neither the mesh nor the module defines is refused.
|
||||
//
|
||||
// **Two lines of work built this at once**, one calling it `Seats` with a protocol and one
|
||||
// `DefinesSeats` without. Same key in the file, so no manifest is affected: this is the trunk's
|
||||
// name with the richer type, because what a role accepts, emits and serves is what lets the mesh
|
||||
// check that a holder answers what its seat promises.
|
||||
DefinesSeats []SeatDeclaration `json:"seats,omitempty"`
|
||||
|
||||
// Uses are seats this module sends to. It names the *seat*, never the module holding it, so
|
||||
// the implementation can be replaced under it and no caller changes. A caller gets publish
|
||||
@@ -392,6 +400,14 @@ type Manifest struct {
|
||||
// that could only see its own ports would write a rule set that closed everything else.
|
||||
Filtering *Filtering `json:"filtering,omitempty"`
|
||||
|
||||
// Jails are the fail2ban jails this module declares for its own service (novox/hq to-be 31).
|
||||
// Written into whichever node runs the module, the same way `listens` become that node's rules.
|
||||
Jails []Jail `json:"jails,omitempty"`
|
||||
|
||||
// Jailing marks the module that composes the node's fail2ban jails — the intrusion-prevention
|
||||
// holder. Like Filtering: one module per node gathers what every module declared and writes it.
|
||||
Jailing *Jailing `json:"jailing,omitempty"`
|
||||
|
||||
// Guards are ports of this module's the mesh refuses on an adopted node except from the
|
||||
// private network and from the machine itself (novox/hq ADR 0100) — the store's port and the
|
||||
// broker's management port. The ports the software uses; the mesh guards where the machine
|
||||
@@ -400,24 +416,29 @@ type Manifest struct {
|
||||
// firewall does. Ignored on a converged node, whose derived filter already closes them.
|
||||
Guards []int `json:"guards,omitempty"`
|
||||
|
||||
// Facts are things only the mesh knows, written where this module asks for them.
|
||||
// Facts are things only the mesh knows, written where this module asks for them — in the
|
||||
// module's own format.
|
||||
//
|
||||
// **The graph is the control plane's; how a machine uses it is the module's.** The mesh knows
|
||||
// which machines exist, what they are called and where they are. Making a name resolve, or a
|
||||
// peer reachable, is somebody's software — dnsmasq, a resolver, a VPN — and the mesh has no
|
||||
// business shipping one, choosing which, or knowing its configuration language.
|
||||
// **The graph is the control plane's; the format is the module's.** The mesh knows which
|
||||
// machines exist, what they are called and where they are. Turning that into a name that
|
||||
// resolves, a peer that is reachable, a host a client trusts, is somebody's software — dnsmasq,
|
||||
// a resolver, a VPN, ssh — in its own configuration language, and the mesh has no business
|
||||
// knowing it. So a module gives a path and a template; the mesh renders the roster through it
|
||||
// and owns nothing of what the file says.
|
||||
//
|
||||
// So a module says *put the node names here* and owns everything after that. The same shape as
|
||||
// `filtering`, generalised: a fact, and a path.
|
||||
// This used to be a closed list of fact names, each formatted in Go in the control plane, so a
|
||||
// new consumer meant a new formatter here in the consumer's language. Now the data is the mesh's
|
||||
// and the format is the module's: the two built-in cases — the network module's `/etc/hosts` and
|
||||
// dnsmasq's zones — render through the same template path any module uses, and no format lives
|
||||
// in the control plane at all. See RosterFile for what a template sees.
|
||||
//
|
||||
// It replaces three modules that existed only because computed output needed somewhere to
|
||||
// live — they ran no software, could not be swapped for anything, and appeared in the graph as
|
||||
// modules while being a data channel wearing a costume.
|
||||
//
|
||||
// Keyed by fact name; the names are a closed list, because a module asking for one the mesh
|
||||
// does not compute is asking for something nobody will write, and finding that out on a machine
|
||||
// is worse than being told here.
|
||||
Facts map[string]string `json:"facts,omitempty"`
|
||||
// Keyed by a name the module chooses, which is the rendered file's id (`fact-<name>`) — what a
|
||||
// `restart-on` names to restart when the roster changes.
|
||||
Facts map[string]RosterFile `json:"facts,omitempty"`
|
||||
|
||||
// Certificate is where this module wants a certificate for its machine's name inside the
|
||||
// mesh, and where the key that goes with it can be found.
|
||||
@@ -650,6 +671,36 @@ func (l Listening) At() string {
|
||||
return l.Protocol
|
||||
}
|
||||
|
||||
// Jail is a fail2ban jail a module declares for its own service (novox/hq to-be 31).
|
||||
//
|
||||
// **The module names no node and no path** (ADR 0112): it says what a break-in on its service looks
|
||||
// like — the failregex — and the jail's own keys (the port it watches, where it logs, how many
|
||||
// tries, how long to ban). The mesh writes it into whichever node's fail2ban runs the module, the
|
||||
// same way a module's `listens` become that node's firewall rules. A node not running the module
|
||||
// has no such jail.
|
||||
type Jail struct {
|
||||
// Name is the jail and its filter, e.g. "postgres-auth". One holder of the name per node.
|
||||
Name string `json:"name"`
|
||||
// Failregex is what a failed authentication looks like in the service's log — the filter.
|
||||
Failregex string `json:"failregex"`
|
||||
// Jail is the body of the jail's stanza: the keys under [<name>] the module knows and the mesh
|
||||
// does not — the port it watches, its logpath and backend, maxretry, bantime.
|
||||
Jail string `json:"jail"`
|
||||
}
|
||||
|
||||
// Jailing says a module composes the node's fail2ban jails — the intrusion-prevention holder. Like
|
||||
// Filtering for the firewall: one module gathers what every other module declared and writes it
|
||||
// where it owns. Into is the one jail file the stanzas are composed into (so the fail2ban service
|
||||
// can restart on a single resource); FilterInto is the directory each jail's filter file goes in.
|
||||
type Jailing struct {
|
||||
Into string `json:"into"`
|
||||
FilterInto string `json:"filter-into"`
|
||||
}
|
||||
|
||||
// ComposedJailsID is the single jail file the mesh composes every declared jail into, so the
|
||||
// fail2ban service names one resource in its restart-on and a jail added or removed reaches it.
|
||||
func ComposedJailsID() string { return "composed-jails" }
|
||||
|
||||
// Filtering says where a module wants the computed rule set.
|
||||
type Filtering struct {
|
||||
// Into is the path to write it to. Whatever loads it is this module's own business — an
|
||||
|
||||
Reference in New Issue
Block a user