162 lines
6.0 KiB
Go
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
|
|
}
|