215 lines
9.0 KiB
Go
215 lines
9.0 KiB
Go
package broker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What the mesh issues an assignment to serve and to reach (novox/hq ADR 0160).
|
|
//
|
|
// A module's code names its tools and its events; **where they land is the mesh's to decide**, and
|
|
// it decided it twice — once in the runtime, once here, by one rule compiled into both. Now the
|
|
// controller composes a membership for every module on every machine and publishes it to a subject
|
|
// only that assignment reads; the runtime serves exactly what the membership says, and the account's
|
|
// grant is the same composition read the other way. The shape issued today is the shape the mesh
|
|
// already had, so nothing moves when a membership first arrives; only who decides it moves.
|
|
|
|
// Membership is one assignment's subjects: what this instance of a module on this machine serves,
|
|
// and what it may reach.
|
|
type Membership struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
// Serves is every address a tool of this instance answers on. `{tool}` stands for the tool's
|
|
// own name, which the module knows and the mesh does not need to: the mesh issues the address,
|
|
// the runtime fills the name. An address with a queue is shared with the module's other
|
|
// instances, and the bus hands each call to one of them; an address without is this instance's.
|
|
Serves []Served `json:"serves"`
|
|
// Seats is every verb of a seat this instance holds, at the subject the seat's callers use.
|
|
Seats []SeatServed `json:"seats,omitempty"`
|
|
// Emits is where an event of this module lands; `{event}` stands for the event's name.
|
|
Emits string `json:"emits"`
|
|
// Reaches is each tool this module may call, `<module>.<tool>`, to the subjects that reach it:
|
|
// the first is whichever instance answers, when the mesh issued one; the rest name a machine.
|
|
Reaches map[string][]string `json:"reaches,omitempty"`
|
|
// Tools is where this instance answers what it serves — the runtime's one verb of its own.
|
|
Tools string `json:"tools"`
|
|
// Receives is what this assignment is given for each requirement it receives, by requirement:
|
|
// the contributions of every module that asked for it, as the catalogue composed them (novox/hq
|
|
// ADR 0167). The same list its received file is written from, so the two cannot disagree; a
|
|
// requirement nobody contributed to is an empty list, never absent. Kept as JSON because the
|
|
// catalogue owns the shape of a contribution and the bus only carries it.
|
|
Receives map[string]json.RawMessage `json:"receives,omitempty"`
|
|
// Mesh is every machine's address on the private network — what a rule saying "from the mesh"
|
|
// resolves to in the packet filter, issued here from the same list (novox/hq ADR 0167). A
|
|
// module that must tell the mesh from the world, the route proxy serving an internal name, reads
|
|
// it here rather than keeping a definition of its own.
|
|
Mesh []string `json:"mesh,omitempty"`
|
|
// State is every bucket this module's code may reach, by the name it uses for each, and whether
|
|
// it may write it (novox/hq ADR 0201): the runtime answers a bundle's state verbs from this list
|
|
// and refuses, with the reason, what is not on it — the bus enforces only the union over every
|
|
// module on the machine.
|
|
State []StateIssued `json:"state,omitempty"`
|
|
// SeatTraffic is what this module's code may submit, say, hear, take, ask, answer and read on seats
|
|
// that name their caller or their kind (novox/hq ADR 0259 §3). The runtime carrying the module
|
|
// publishes, takes and answers for it only what is listed here: the bus enforces only the union
|
|
// over every module on the machine, so one module's code reaching another's name or kind through
|
|
// the runtime is the runtime's to refuse.
|
|
SeatTraffic *SeatTraffic `json:"seat-traffic,omitempty"`
|
|
}
|
|
|
|
// Served is one address a tool is answered on.
|
|
type Served struct {
|
|
Subject string `json:"subject"`
|
|
Queue string `json:"queue,omitempty"`
|
|
}
|
|
|
|
// SeatServed is one verb of a held seat, where its callers ask.
|
|
type SeatServed struct {
|
|
Seat string `json:"seat"`
|
|
Verb string `json:"verb"`
|
|
Subject string `json:"subject"`
|
|
}
|
|
|
|
// MembershipSubject is the one address a runtime derives for itself: where its own membership is
|
|
// published, from the two names its credential carries. Everything else is in the membership.
|
|
func MembershipSubject(node, module string) string {
|
|
return "mesh.assignment." + node + "." + module
|
|
}
|
|
|
|
// Placements is where every module runs, for deciding which instance answers for the module.
|
|
type Placements struct {
|
|
// Nodes is each module's machines.
|
|
Nodes map[string][]string
|
|
// Interchangeable is each module whose definition says its instances are the same anywhere,
|
|
// so the module's plain subject is issued to all of them in one queue.
|
|
Interchangeable map[string]bool
|
|
// Kinds is every kind held of a kinded bench, by whom and with what capabilities (ADR 0259 §5).
|
|
Kinds []KindHeld
|
|
}
|
|
|
|
// AnswersForTheModule says whether an instance of a module on one machine is issued the module's
|
|
// plain subject: when it is the only instance, or when the definition says instances are
|
|
// interchangeable. A stateful module on two machines gets only its machines' subjects, so a call
|
|
// that names none reaches nothing rather than the wrong store.
|
|
func (p Placements) AnswersForTheModule(module string) bool {
|
|
return len(p.Nodes[module]) <= 1 || p.Interchangeable[module]
|
|
}
|
|
|
|
// MembershipFor composes one assignment's membership from what it declared and where everything
|
|
// runs. The subjects are the ones PermissionsFor grants, derived here once more only until the
|
|
// grant itself is read from the membership — which is the next step, not this one.
|
|
func MembershipFor(node string, d Declared, where Placements) Membership {
|
|
own := "mesh.mod." + d.Module
|
|
m := Membership{
|
|
Node: node, Module: d.Module,
|
|
Emits: own + ".event.{event}",
|
|
Tools: own + ".tool.tools",
|
|
}
|
|
// This machine's address always; the module's when this instance answers for the module.
|
|
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}." + node})
|
|
if where.AnswersForTheModule(d.Module) {
|
|
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module})
|
|
}
|
|
for _, s := range d.Holds {
|
|
for _, verb := range s.Serves {
|
|
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
|
|
}
|
|
}
|
|
m.State = stateIssuedFor(d, node)
|
|
t := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches)
|
|
for _, s := range append(append([]Seat{}, d.Uses...), d.Watches...) {
|
|
if !s.Kinded {
|
|
continue
|
|
}
|
|
for _, k := range where.Kinds {
|
|
if k.Seat == s.Name && !kindListed(t.Kinds, k) {
|
|
t.Kinds = append(t.Kinds, k)
|
|
}
|
|
}
|
|
}
|
|
if len(t.Publish)+len(t.Subscribe)+len(t.Answers)+len(t.Workers)+len(t.Records)+len(t.Kinds) > 0 {
|
|
m.SeatTraffic = &t
|
|
}
|
|
if len(d.Invokes) > 0 {
|
|
m.Reaches = map[string][]string{}
|
|
for _, t := range d.Invokes {
|
|
if t == "*" || strings.HasPrefix(t, "seat:") {
|
|
continue // every tool, or a role's: addressed by name, not resolved per instance
|
|
}
|
|
module, tool, ok := strings.Cut(t, ".")
|
|
if !ok {
|
|
continue
|
|
}
|
|
var reach []string
|
|
if where.AnswersForTheModule(module) {
|
|
reach = append(reach, "mesh.mod."+module+".tool."+tool)
|
|
}
|
|
nodes := append([]string{}, where.Nodes[module]...)
|
|
sort.Strings(nodes)
|
|
for _, n := range nodes {
|
|
reach = append(reach, "mesh.mod."+module+".tool."+tool+"."+n)
|
|
}
|
|
m.Reaches[t] = reach
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// PlacementsOf reads where everything runs from the records the bus's accounts are composed from.
|
|
func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
|
|
p := Placements{Nodes: map[string][]string{}, Interchangeable: interchangeable}
|
|
for node, declared := range r.Assigned {
|
|
for _, d := range declared {
|
|
p.Nodes[d.Module] = append(p.Nodes[d.Module], node)
|
|
}
|
|
}
|
|
for _, nodes := range p.Nodes {
|
|
sort.Strings(nodes)
|
|
}
|
|
for node, declared := range r.Assigned {
|
|
for _, d := range declared {
|
|
for _, s := range d.Holds {
|
|
if s.Kinded && s.Kind != "" {
|
|
p.Kinds = append(p.Kinds, KindHeld{Seat: s.Name, Kind: s.Kind, Module: d.Module, Node: node,
|
|
Capabilities: placedCapabilities(s.Capabilities, d.RunsAs)})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(p.Kinds, func(i, j int) bool {
|
|
a, b := p.Kinds[i], p.Kinds[j]
|
|
if a.Seat != b.Seat {
|
|
return a.Seat < b.Seat
|
|
}
|
|
if a.Kind != b.Kind {
|
|
return a.Kind < b.Kind
|
|
}
|
|
return a.Node < b.Node
|
|
})
|
|
return p
|
|
}
|
|
|
|
// placedCapabilities is what a kind's claim promises, as far as its placement lets the router believe it
|
|
// (novox/hq ADR 0259 §8): `verified-sender` only from a holder that runs as an account of its own, on a bus
|
|
// account of its own — never one the machine's runtime carries as the operator's account.
|
|
func placedCapabilities(declared []string, runsAs string) []string {
|
|
var out []string
|
|
for _, c := range declared {
|
|
if c == "verified-sender" && runsAs == "" {
|
|
continue
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func kindListed(list []KindHeld, k KindHeld) bool {
|
|
for _, x := range list {
|
|
if x.Seat == k.Seat && x.Kind == k.Kind && x.Module == k.Module && x.Node == k.Node {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|