The part composed every membership with no root-free input, so telegram's verified-sender was dropped and the membership held as a shadow difference. It now asks mesh-controller.root-free for the machines a composition needs, per pass, failing closed and said once, and wakes on root-not-free condition-raised and condition-cleared.
123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
)
|
|
|
|
// CallRootFree is the controller seat's verb that judges machines root-free (novox/hq ADR 0259 §8): the one judgement
|
|
// the composer needs and the part cannot make from the bus (issue 1945). Only the controller answers it, on purpose.
|
|
const CallRootFree = "seat:mesh-controller.root-free"
|
|
|
|
// The controller seat's 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.
|
|
const (
|
|
EventConditionRaised = "mesh-controller.condition-raised"
|
|
EventConditionCleared = "mesh-controller.condition-cleared"
|
|
// KindRootNotFree is the condition kind whose events wake a pass.
|
|
KindRootNotFree = "root-not-free"
|
|
)
|
|
|
|
// Consumes is every event the part hears, as its manifest's consumes name them.
|
|
var Consumes = []string{EventConditionRaised, EventConditionCleared}
|
|
|
|
// 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 the controller'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 the controller once, and answers a
|
|
// line for each machine that is not free, `<machine>: <why>`. It fails closed: an error, no answer or a machine
|
|
// left out is not free, and says so. Pure but for the one call; the answer is 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{}
|
|
var failure string
|
|
if call == nil {
|
|
failure = "no way to ask the controller here"
|
|
} else if raw, err := call.Call(CallRootFree, map[string]any{"machines": nodes}); err != nil {
|
|
failure = "the controller's root-free word could not be asked: " + err.Error()
|
|
} else {
|
|
var body struct {
|
|
Machines []verdict `json:"machines"`
|
|
}
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
failure = "the controller's root-free answer could not be read: " + err.Error()
|
|
}
|
|
for _, v := range body.Machines {
|
|
free[v.Machine] = 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 failure != "":
|
|
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failure))
|
|
case !answered:
|
|
notes = append(notes, fmt.Sprintf("%s: not free, the controller named no verdict for it", n))
|
|
default:
|
|
notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why))
|
|
}
|
|
}
|
|
return notes
|
|
}
|