Issue 127 stood because nothing compared the two halves. Every manifest was well-formed on its own and every derivation correct on its own, and no cross-module subscription in the mesh matched anything — a subscription that matches nothing is not an error, it is silence. Two checks, because the mistake is possible at two scales. Per manifest: an event is a local name, and `module.` is refused with the name to write instead. A module emitting under what reads as another module's name is refused too, pointing at the seat, where a name outlives whoever holds it. Across the catalogue: where a consumed event's emitter is present, it must emit that event. It cannot demand a live emitter for everything — a module lives in its own repository and may be installed long before the one whose events it wants — so the rule is narrower and still catches this. It found two real dangling subscriptions the moment it ran. Wildcards were undecided and two manifests needed them: `*` is one name and `**` is the rest, spelled the mesh's way and derived to `>` here and `#` on the old bus. A manifest naming either would stop being true when the wire changed, which is the whole reason names are local. And the field documentation taught the old form, examples included — which is why the drift was uniform across 37 manifests rather than scattered. Nobody was guessing; everybody followed the comment.
135 lines
5.1 KiB
Go
135 lines
5.1 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// What the bus's user list is derived from, read out of the mesh's records.
|
|
//
|
|
// The deriving itself is pure and lives in the broker package; this is the reading, and it is kept
|
|
// apart for the reason that package keeps its own types: a permission must be a function of what a
|
|
// module declared, and a query that decided anything would be a second place authority came from.
|
|
|
|
// BusRecords is every fact the composer needs about who may reach the bus.
|
|
//
|
|
// **A module's authority comes from the manifest, not from the assignment.** The assignment says
|
|
// *where* it runs; what it may say is in what it declared, so the two are read together and the
|
|
// manifest is the one that decides.
|
|
func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
|
|
nodes, err := i.Nodes(ctx)
|
|
if err != nil {
|
|
return broker.Records{}, fmt.Errorf("cannot read the mesh's machines: %w", err)
|
|
}
|
|
declared, err := i.Catalogue(ctx)
|
|
if err != nil {
|
|
return broker.Records{}, fmt.Errorf("cannot read the catalogue: %w", err)
|
|
}
|
|
|
|
// Every seat any module declares, by name, so a module's claim can be resolved to the protocol
|
|
// that seat promises. **Across the whole catalogue, not one manifest**: a seat is declared by
|
|
// one module and held by another, which is the whole reason a seat exists (ADR 0118).
|
|
seats := map[string]catalogue.SeatDeclaration{}
|
|
for _, m := range declared {
|
|
for _, s := range m.Seats {
|
|
seats[s.Name] = s
|
|
}
|
|
}
|
|
|
|
out := broker.Records{Assigned: map[string][]broker.Declared{}, People: map[string][]string{}}
|
|
for _, n := range nodes {
|
|
out.Nodes = append(out.Nodes, n.Name)
|
|
modules, err := i.Assigned(ctx, n.Name)
|
|
if err != nil {
|
|
return broker.Records{}, fmt.Errorf("cannot read what %s runs: %w", n.Name, err)
|
|
}
|
|
for _, module := range modules {
|
|
m, known := declared[module]
|
|
if !known {
|
|
// Assigned and not in the catalogue. Said rather than composed with no authority:
|
|
// a user with an empty permission list is a module that starts, connects, and is
|
|
// refused by the server on its first publish — an authorisation error that says
|
|
// nothing about a missing manifest.
|
|
//
|
|
// **The catalogue refuses to forget an assigned module, so this is the second line
|
|
// and not the first.** It earns its place there anyway: relying on another
|
|
// package's invariant is how a rule ends up enforced by nothing.
|
|
return broker.Records{}, fmt.Errorf(
|
|
"%s is assigned to %s and is not in the catalogue, so what it may say cannot "+
|
|
"be derived", module, n.Name)
|
|
}
|
|
out.Assigned[n.Name] = append(out.Assigned[n.Name], declaredFor(m, seats))
|
|
}
|
|
}
|
|
|
|
enrolling, err := i.NodesWithALiveToken(ctx)
|
|
if err != nil {
|
|
return broker.Records{}, err
|
|
}
|
|
out.Enrolling = enrolling
|
|
|
|
// People are not recorded yet: the account model is built (design 25 §7's first item) and
|
|
// `operator issue` is not, so there is nobody to derive. Left empty rather than guessed at.
|
|
return out, nil
|
|
}
|
|
|
|
// declaredFor is one module's manifest as the composer needs it: what it says about itself, and the
|
|
// protocol of every seat it holds or uses.
|
|
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration) broker.Declared {
|
|
d := broker.Declared{
|
|
Module: m.Module,
|
|
Emits: m.Emits,
|
|
Consumes: m.Consumes,
|
|
// The tools it answers, which is `tools` and not `serves`: the manifest's `serves` is the
|
|
// facts a consumer needs to reach a provision, a different meaning under a similar word.
|
|
Serves: m.Tools,
|
|
}
|
|
for _, c := range m.Claims {
|
|
// A seat the mesh defines for itself carries no protocol, so holding one grants nothing here:
|
|
// those seats say who does a job, not who may say what.
|
|
if s, declaredSomewhere := seats[c.Name]; declaredSomewhere {
|
|
d.Holds = append(d.Holds, asSeat(s))
|
|
}
|
|
}
|
|
for _, name := range m.Uses {
|
|
if s, declaredSomewhere := seats[name]; declaredSomewhere {
|
|
d.Uses = append(d.Uses, asSeat(s))
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
func asSeat(s catalogue.SeatDeclaration) broker.Seat {
|
|
return broker.Seat{Name: s.Name, Accepts: s.Accepts, Emits: s.Emits, Serves: s.Serves}
|
|
}
|
|
|
|
// NodesWithALiveToken is every machine holding a token that could still be presented — issued, not
|
|
// expired, not redeemed.
|
|
//
|
|
// **One enrolment user per such token** (design 25 §6): the inbox an answer goes to is scoped to the
|
|
// token, because an answer carries that machine's credentials sealed to it and a shared inbox is one
|
|
// machine able to read another's.
|
|
func (i *Inventory) NodesWithALiveToken(ctx context.Context) ([]string, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select distinct n.name
|
|
from enrolment_token t join node n on n.id = t.node
|
|
where t.redeemed is null and t.expires > now()
|
|
order by n.name`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read which machines hold a live token: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []string
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, name)
|
|
}
|
|
return out, rows.Err()
|
|
}
|