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.
156 lines
6.0 KiB
Go
156 lines
6.0 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
|
|
}
|
|
}
|
|
// And the mesh's own, which carry protocol too (novox/hq ADR 0121). Added after the modules'
|
|
// rather than before, because a `mesh-*` name is the mesh's and registration refuses a module
|
|
// declaring one — so this cannot be shadowed, and if it ever were, the mesh's own would win.
|
|
for _, own := range catalogue.SeatsWithAProtocol() {
|
|
seats[own.Name] = catalogue.SeatDeclaration{
|
|
Name: own.Name, Scope: own.Scope,
|
|
Accepts: own.Accepts, Emits: own.Emits, Serves: own.Serves,
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// Every seat with a protocol, the mesh's own included. One that says only who does a job is
|
|
// not here and grants nothing, which is most of them.
|
|
if s, hasAProtocol := seats[c.Name]; hasAProtocol {
|
|
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}
|
|
}
|
|
|
|
// MeshSeats are the mesh's own seats that carry a protocol, as the bus needs them: what to make a work
|
|
// queue for, and whose holder gets a worker on it (novox/hq ADR 0121).
|
|
func MeshSeats() []broker.DeclaredSeat {
|
|
var out []broker.DeclaredSeat
|
|
for _, s := range catalogue.SeatsWithAProtocol() {
|
|
out = append(out, broker.DeclaredSeat{
|
|
Name: s.Name, Accepts: s.Accepts, Emits: s.Emits, Serves: s.Serves,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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()
|
|
}
|