Files
mesh-delivery/modules/mesh-grants/internal/grants/rootfree.go
T

162 lines
6.0 KiB
Go

package grants
import (
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/health"
)
// 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 controller seat's until the operator's take-over, and the
// condition keeper's (mesh-conditions) from it: the part hears only the seat the take-over key names, never both (novox/hq
// ADR 0564 §9, issue 1952 G1).
const (
EventConditionRaised = "mesh-controller.condition-raised"
EventConditionCleared = "mesh-controller.condition-cleared"
EventKeeperRaised = health.KeeperModule + ".condition-raised"
EventKeeperCleared = health.KeeperModule + ".condition-cleared"
// KindRootNotFree is the condition kind whose events wake a pass.
KindRootNotFree = "root-not-free"
)
// ReadTakeover is the condition keeper's take-over key's state (`switch`): which seat's condition events the part hears.
const ReadTakeover = health.KeeperModule + "." + health.TakeoverState
// HeardFrom is the seat a condition event heard comes from, by its name (`<seat>.<event>`) or its bus subject
// (`mesh.seat.<seat>.event.<event>`); empty for any other.
func HeardFrom(event string) string {
for _, seat := range []string{health.KeeperModule, "mesh-controller"} {
if strings.HasPrefix(event, seat+".") || strings.HasPrefix(event, "mesh.seat."+seat+".event.") {
return seat
}
}
return ""
}
// 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.
var Consumes = []string{EventConditionRaised, EventConditionCleared, EventKeeperRaised, EventKeeperCleared, EventAccountIssued}
// 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
}
// EventSubject is the bus subject of a controller seat event named `mesh-controller.<event>`.
func EventSubject(event string) string {
return "mesh.seat.mesh-controller.event." + strings.TrimPrefix(event, "mesh-controller.")
}
// 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
}