Files
mesh-delivery/modules/mesh-grants/internal/grants/inputs.go
T
jschoubben b4af9cdb7a mesh-grants publishes only a first or an additive membership, and holds no lock across the bus (review of 1ff5049)
Composed without a root-free judgement, a membership would drop a kind's verified-sender and the
operator's channel approvals would stop being honoured; any loss now stays a shadow difference,
named. A tool no longer waits on a pass's round trip. compose-now says the controller serves no
verb to write a module's grant again.
2026-10-25 22:52:00 +01:00

330 lines
12 KiB
Go

// Package grants is the grants part in shadow (novox/hq ADR 0547 clauses 5 to 8, issue 1945): it composes the bus's user
// list, each module's membership and each manifest's buckets, logs and streams from what the bus holds, with the bus
// composer the controller uses (mesh-sdk buscompose), and compares them with what the bus loaded and holds. It writes only
// its own state and takes no effect: it publishes no membership, asserts nothing, writes no grant and no user list the
// bus node reads, until the operator's take-over, which this milestone always refuses.
package grants
import (
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The states of other parts the grants part reads, as its manifest's reads name them (`<module>.<state>`).
const (
ReadAssignments = "mesh-controller.assignments"
ReadControllerGrant = "mesh-controller.grants"
ReadModules = "mesh-delivery-indexer.modules"
ReadManifests = "mesh-delivery-indexer.manifests"
ReadHave = "node-engine.have"
ReadResolved = "mesh-config.resolved"
ReadWants = "mesh-delivery-assigner.wants"
ReadBuilds = "mesh-delivery-builder.builds"
)
// Reads is every state the part reads, in the manifest's order.
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds}
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
const (
CallUsers = "nats.nats_users"
CallUserCan = "nats.nats_user_can"
CallStreams = "nats.nats_streams"
)
// Invokes is every tool the part calls.
var Invokes = []string{CallUsers, CallUserCan, CallStreams}
// Reader reads another part's state, named `<module>.<state>`. A refusal (a read not granted yet, a bucket not there) is
// an error the part names as a gap and composes past: it never stops for one.
type Reader interface {
Keys(state string) ([]string, error)
Get(state, key string) ([]byte, bool, error)
}
// Caller calls another module's tool, `<module>.<tool>`, and answers its result.
type Caller interface {
Call(key string, body any) (json.RawMessage, error)
}
// Gap is an input the shadow cannot read from the bus, named with why: never guessed.
type Gap struct {
Input string `json:"input"`
Why string `json:"why"`
}
// Standing gaps: what the controller composes from its own store, which no bus value carries in this milestone.
var standingGaps = []Gap{
{Input: "enrolment tokens", Why: "the live enrolment tokens are in the controller's store alone: no enrolment user is composed"},
{Input: "people", Why: "the people and the tools they may invoke are in the controller's store alone: no person user is composed"},
{Input: "seat holdings", Why: "who holds each seat held once for the mesh is in the controller's store alone: a mesh seat claimed on two machines is composed as held on both"},
{Input: "announced states", Why: "the controller does not announce its states-announced readable, so every read is composed from `reads`, as when announcements cannot be read"},
{Input: "credentials", Why: "the shadow holds no hash, so a user it composes that the loaded list lacks may be one left out for want of a credential"},
}
// assignment is a key of the controller's assignments as composing needs it (mesh-controller internal/chain Assignment):
// a value that names no time assigned (a module pulled in), has ended, or waits in a state, is no assignment.
type assignment struct {
Node string `json:"node"`
Module string `json:"module"`
Assigned time.Time `json:"assigned,omitzero"`
Ended time.Time `json:"ended,omitzero"`
State string `json:"state,omitempty"`
}
func (a assignment) stands() bool { return !a.Assigned.IsZero() && a.Ended.IsZero() && a.State == "" }
// resolved is a key of mesh-config's resolved values, `<node>.<module>.<key>`.
type resolved struct {
Node string `json:"node"`
Module string `json:"module"`
Key string `json:"key"`
Value json.RawMessage `json:"value,omitempty"`
}
// Inputs is what the shadow composes from, read from the bus.
type Inputs struct {
// Nodes is every machine seen: in a node agent's have or a standing assignment.
Nodes []string
// Assigned is each machine's standing assignments, sorted.
Assigned map[string][]string
// Manifests is each module's manifest at the indexer's trunk, by module.
Manifests map[string]catalogue.Manifest
// Digests is each module's manifest digest, by module.
Digests map[string]string
// Running is the manifest a machine is wanted to run, by `<node>.<module>`: the one the builder built at the
// fingerprint the assigner's want names. An assignment with none is composed from the trunk's manifest.
Running map[string]catalogue.Manifest
// Values is each resolved config value, by `<node>/<module>/<key>`.
Values map[string]any
// BusNode is the machine running the bus's module, and Loaded what its node agent says it loaded.
BusNode string
Loaded *delivery.UserListWritten
Gaps []Gap
}
// BusModule is the bus's module: it holds mesh-broker.
const BusModule = "nats"
// Manifests caches manifests by digest: a digest names one manifest for ever.
type Manifests struct {
mu sync.Mutex
parsed map[string]catalogue.Manifest
}
func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) {
// The lock guards the map alone, never a read on the bus: a tool never waits on a pass's round trip.
c.mu.Lock()
m, ok := c.parsed[digest]
c.mu.Unlock()
if ok {
return m, nil
}
raw, found, err := r.Get(ReadManifests, digest)
if err != nil {
return catalogue.Manifest{}, err
}
if !found {
return catalogue.Manifest{}, fmt.Errorf("the indexer holds no manifest %s", digest)
}
v, err := delivery.DecodeManifest(raw)
if err != nil {
return catalogue.Manifest{}, err
}
m, err = catalogue.ParseManifest([]byte(v.JSON))
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("the manifest %s is refused by mesh-sdk's reader: %w", digest, err)
}
c.mu.Lock()
defer c.mu.Unlock()
if c.parsed == nil {
c.parsed = map[string]catalogue.Manifest{}
}
c.parsed[digest] = m
return m, nil
}
// each reads every current value of a state; a refusal is a gap and an empty map.
func each(r Reader, state string, gaps *[]Gap) map[string][]byte {
keys, err := r.Keys(state)
if err != nil {
*gaps = append(*gaps, Gap{Input: state, Why: "refused or absent: " + err.Error()})
return map[string][]byte{}
}
out := map[string][]byte{}
for _, k := range keys {
v, found, err := r.Get(state, k)
if err != nil {
*gaps = append(*gaps, Gap{Input: state + " " + k, Why: err.Error()})
continue
}
if found {
out[k] = v
}
}
return out
}
// ReadInputs reads what the shadow composes from. It never fails: what it cannot read is a gap.
func ReadInputs(r Reader, cache *Manifests) Inputs {
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{},
Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
nodes := map[string]bool{}
for _, raw := range each(r, ReadAssignments, &in.Gaps) {
var a assignment
if json.Unmarshal(raw, &a) != nil || !a.stands() || a.Node == "" || a.Module == "" {
continue
}
nodes[a.Node] = true
in.Assigned[a.Node] = append(in.Assigned[a.Node], a.Module)
if a.Module == BusModule {
in.BusNode = a.Node
}
}
haveKeys, err := r.Keys(ReadHave)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadHave, Why: "refused or absent: " + err.Error()})
}
for _, k := range haveKeys {
if node, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode {
nodes[node] = true
}
}
in.Gaps = append(in.Gaps, Gap{Input: "machines", Why: "read from the node agents' haves and the assignments: a machine with neither is not seen"})
for n := range nodes {
in.Nodes = append(in.Nodes, n)
}
sort.Strings(in.Nodes)
if in.BusNode != "" {
if raw, found, err := r.Get(ReadHave, in.BusNode+"."+BusModule); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: "the loaded list's have", Why: err.Error()})
} else if found {
if h, err := delivery.DecodeHave(raw); err == nil {
in.Loaded = h.UserList
}
}
}
wanted := map[string]bool{}
for node := range in.Assigned {
sort.Strings(in.Assigned[node])
for _, m := range in.Assigned[node] {
wanted[m] = true
}
}
modules := make([]string, 0, len(wanted))
for m := range wanted {
modules = append(modules, m)
}
sort.Strings(modules)
// The seats every module declares, across the whole catalogue: read every module the indexer holds, not only the
// assigned ones, since a seat is declared by one module and held by another.
indexed, err := r.Keys(ReadModules)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadModules, Why: "refused or absent: " + err.Error()})
}
for _, module := range indexed {
raw, found, err := r.Get(ReadModules, module)
if err != nil || !found {
continue
}
v, err := delivery.DecodeModule(raw)
if err == nil && v.Manifest == "" {
err = fmt.Errorf("the indexer's value names no manifest digest")
}
if err != nil {
if wanted[module] {
in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + module, Why: err.Error()})
}
continue
}
m, err := cache.get(r, v.Manifest)
if err != nil {
if wanted[module] {
in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + module, Why: err.Error()})
}
continue
}
in.Manifests[module] = m
in.Digests[module] = v.Manifest
}
for _, module := range modules {
if _, ok := in.Manifests[module]; !ok && !gapNamed(in.Gaps, "manifest of "+module) {
in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + module,
Why: "assigned, and the indexer holds no manifest of it at the trunk (a module registered from outside the indexed repositories): composed with no authority"})
}
}
// What each machine is wanted to run: the want's fingerprint, the builder's build of it, the manifest it built.
var fromTrunk []string
for key, raw := range each(r, ReadWants, &in.Gaps) {
node, module, err := delivery.ParseWantKey(key)
if err != nil || !slices.Contains(in.Assigned[node], module) {
continue
}
w, err := delivery.DecodeWant(raw)
if err != nil {
fromTrunk = append(fromTrunk, node+"."+module)
continue
}
if w.Removed || w.FP == "" {
continue
}
bk, err := delivery.BuilderBuildKey(module, w.FP)
if err != nil {
continue
}
braw, found, err := r.Get(ReadBuilds, bk)
if err != nil || !found {
fromTrunk = append(fromTrunk, node+"."+module)
continue
}
b, err := delivery.DecodeBuilderBuild(braw)
if err != nil || b.Manifest == "" {
fromTrunk = append(fromTrunk, node+"."+module)
continue
}
if m, err := cache.get(r, b.Manifest); err == nil {
in.Running[node+"."+module] = m
} else {
fromTrunk = append(fromTrunk, node+"."+module)
}
}
if len(fromTrunk) > 0 {
sort.Strings(fromTrunk)
in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")})
}
for _, raw := range each(r, ReadResolved, &in.Gaps) {
var v resolved
if json.Unmarshal(raw, &v) != nil || v.Node == "" || v.Module == "" || v.Key == "" || len(v.Value) == 0 {
continue
}
var value any
if json.Unmarshal(v.Value, &value) == nil {
in.Values[v.Node+"/"+v.Module+"/"+v.Key] = value
}
}
sortGaps(in.Gaps)
return in
}
func gapNamed(gaps []Gap, input string) bool {
for _, g := range gaps {
if g.Input == input {
return true
}
}
return false
}
func sortGaps(g []Gap) {
sort.SliceStable(g, func(i, j int) bool { return strings.Compare(g[i].Input, g[j].Input) < 0 })
}