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() }