185 lines
7.2 KiB
Go
185 lines
7.2 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
"mesh-grants/internal/buscompose"
|
|
)
|
|
|
|
// CallRootFree is the node seat's verb that judges one machine root-free (novox/hq ADR 0259 §8, ADR 0563 §6), asked of
|
|
// that machine's own engine as `seat:node-engine.root-free@<machine>`: the judgement the composer needs and the part
|
|
// cannot make from the bus (issue 1945). Only that machine's engine answers it, and the controller takes no part in it
|
|
// (ADR 0571 §11, issue 1955). The messenger asks the same verb the same way.
|
|
const CallRootFree = "seat:node-engine.root-free"
|
|
|
|
// The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the
|
|
// clearing of its `root-not-free` condition. They are the condition keeper's (mesh-conditions) alone, the one keeper
|
|
// since the controller ended by its unassignment (novox/hq ADR 0590 §3, which ends ADR 0564 §9's selection by the
|
|
// take-over key).
|
|
const (
|
|
EventKeeperRaised = meshconditions.Seat + ".condition-raised"
|
|
EventKeeperCleared = meshconditions.Seat + ".condition-cleared"
|
|
// KindRootNotFree is the condition kind whose events wake a pass.
|
|
KindRootNotFree = "root-not-free"
|
|
)
|
|
|
|
// FromKeeper says a condition event heard is the keeper's, by its name (`<seat>.<event>`) or its bus subject
|
|
// (`mesh.seat.<seat>.event.<event>`).
|
|
func FromKeeper(event string) bool {
|
|
return strings.HasPrefix(event, meshconditions.Seat+".") || strings.HasPrefix(event, "mesh.seat."+meshconditions.Seat+".event.")
|
|
}
|
|
|
|
// EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's
|
|
// bus-accounts, so a pass composes the list with it.
|
|
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
|
|
|
|
// Consumes is every event the part hears, as its manifest's consumes name them.
|
|
// The seat mesh-broker's created, recreated and dropped are heard as every seat's event, `<seat>.<event>` on EVENTS (the
|
|
// lead's ruling, registry milestone 13:0x), never on the bus module's own stream.
|
|
var Consumes = append(append(slices.Clone(seatEvents), seatListEvents...), viewsEvents()...)
|
|
|
|
// seatListEvents are the two events of the seat mesh-catalog the seat list reader follows (mesh-sdk seatlist), heard by it on the
|
|
// module's event hub, so no handler of the hub takes them besides.
|
|
var seatListEvents = []string{meshcatalog.Seat + "." + meshcatalog.EventSeatDefinitionChanged, meshcatalog.Seat + "." + meshcatalog.EventSeatDefinitionRemoved}
|
|
|
|
// seatEvents are the events of seats the part follows its passes by, as `<seat>.<event>`.
|
|
var seatEvents = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, meshbroker.Seat + "." + BrokerCreated,
|
|
meshbroker.Seat + "." + BrokerDropped, meshbroker.Seat + "." + BrokerRecreated}
|
|
|
|
// SeatEventSubjects are the subjects of the seats' events the part follows its passes by besides the verdicts' and the
|
|
// views': the keeper's condition events, the vault's account-issued and the seat mesh-broker's, each `<seat>.<event>` of
|
|
// Consumes on the seat's own subject.
|
|
func SeatEventSubjects() []string {
|
|
var out []string
|
|
for _, c := range seatEvents {
|
|
seat, event, _ := strings.Cut(c, ".")
|
|
out = append(out, "mesh.seat."+seat+".event."+event)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// viewsEvents are the owners' events the part follows its first views by: `<owner>.views.<state>`, but the verdicts by
|
|
// the indexer's manifest-accepted and manifest-refused (VerdictEvents, ADR 0622 §2).
|
|
func viewsEvents() []string {
|
|
var out []string
|
|
for _, fv := range FirstViews {
|
|
if fv.Read == ReadVerdicts {
|
|
out = append(out, VerdictEvents...)
|
|
continue
|
|
}
|
|
out = append(out, fv.Owner+".views."+fv.State)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject.
|
|
func IsAccountIssued(event string) bool {
|
|
return event == EventAccountIssued || event == buscompose.AccountIssuedSubject
|
|
}
|
|
|
|
// IsRootNotFree says a condition event's body names the `root-not-free` kind (its `kind`, or the last part of its
|
|
// `key`, `<scope>.<id>.<kind>`). An unreadable body wakes a pass too: a pass is cheap, a missed change is not.
|
|
func IsRootNotFree(body []byte) bool {
|
|
var c struct {
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key"`
|
|
}
|
|
if json.Unmarshal(body, &c) != nil {
|
|
return true
|
|
}
|
|
return c.Kind == KindRootNotFree || (c.Kind == "" && strings.HasSuffix(c.Key, "."+KindRootNotFree)) || (c.Kind == "" && c.Key == "")
|
|
}
|
|
|
|
// RootFreeNodes are the machines a composition needs a root-free word for: where the router's seat is held and where a
|
|
// kinded seat's holder declares `verified-sender`. None when no holder declares it. Sorted. Pure.
|
|
func RootFreeNodes(r buscompose.Records) []string {
|
|
var nodes []string
|
|
need := false
|
|
for node, declared := range r.Assigned {
|
|
for _, d := range declared {
|
|
for _, s := range d.Holds {
|
|
if s.Name == "operator-channel" && !slices.Contains(nodes, node) {
|
|
nodes = append(nodes, node)
|
|
}
|
|
if s.Kinded && s.Kind != "" && slices.Contains(s.Capabilities, "verified-sender") {
|
|
need = true
|
|
if !slices.Contains(nodes, node) {
|
|
nodes = append(nodes, node)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !need {
|
|
return nil
|
|
}
|
|
sort.Strings(nodes)
|
|
return nodes
|
|
}
|
|
|
|
// verdict is one machine's answer of its engine's root-free verb.
|
|
type verdict struct {
|
|
Machine string `json:"machine"`
|
|
Free bool `json:"free"`
|
|
Why string `json:"why"`
|
|
}
|
|
|
|
// WithRootFree fills r.RootFree for the machines the composition needs by asking each machine's own engine about that
|
|
// machine, and answers a line for each machine that is not free, `<machine>: <why>`. It fails closed: an error, no
|
|
// answer or a verdict an engine gives on another machine is not free, and says so. Pure but for the calls; the answers
|
|
// are kept for this pass alone.
|
|
func WithRootFree(r *buscompose.Records, call Caller) []string {
|
|
nodes := RootFreeNodes(*r)
|
|
if len(nodes) == 0 {
|
|
return nil
|
|
}
|
|
free := map[string]verdict{}
|
|
failures := map[string]string{}
|
|
for _, n := range nodes {
|
|
if call == nil {
|
|
failures[n] = "no way to ask the machine's engine here"
|
|
continue
|
|
}
|
|
raw, err := call.Call(CallRootFree+"@"+n, map[string]any{})
|
|
if err != nil {
|
|
failures[n] = "the engine's root-free word could not be asked: " + err.Error()
|
|
continue
|
|
}
|
|
var body struct {
|
|
Machines []verdict `json:"machines"`
|
|
}
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
failures[n] = "the engine's root-free answer could not be read: " + err.Error()
|
|
continue
|
|
}
|
|
for _, v := range body.Machines {
|
|
if v.Machine == n {
|
|
free[n] = v
|
|
}
|
|
}
|
|
}
|
|
r.RootFree = map[string]bool{}
|
|
var notes []string
|
|
for _, n := range nodes {
|
|
v, answered := free[n]
|
|
switch {
|
|
case answered && v.Free:
|
|
r.RootFree[n] = true
|
|
case failures[n] != "":
|
|
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failures[n]))
|
|
case !answered:
|
|
notes = append(notes, fmt.Sprintf("%s: not free, its engine named no verdict for it", n))
|
|
default:
|
|
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why))
|
|
}
|
|
}
|
|
return notes
|
|
}
|