mesh/delivery merged: source.main moved to novox/mesh-delivery@7ddbf40715ff
mesh/check pass aea22bf793b4 on 7ddbf40715ff | unit 168 run, 479 kept | integration 178 run, 545 kept | composition passed on novox
The part reads seats, buckets, logs and streams, never a config value, and the merge gate already judged the fallbacks; reading the controller's predefined mirror would tie the part to the leaving controller (ADR 0547 clause 1). ParseManifestForInputs takes a fallback as it declares itself. Issue: 1945
364 lines
14 KiB
Go
364 lines
14 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. Beyond its own
|
|
// state it takes effect in two things alone, each by a grant the operator allowed: it publishes memberships (2026-10-25
|
|
// 22:31), and creates a bucket, log or stream a manifest declares that the bus lacks (2026-10-26 08:54, issue 1790). It
|
|
// changes no stream the bus holds, 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"
|
|
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
|
|
ReadAccounts = "mesh-vault.bus-accounts"
|
|
)
|
|
|
|
// Reads is every state the part reads, in the manifest's order.
|
|
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
|
ReadAccounts}
|
|
|
|
// 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"},
|
|
}
|
|
|
|
// 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
|
|
// Hashes are the vault's hashes of the bus accounts, by bus user (ReadAccounts); kept in memory, never written.
|
|
Hashes map[string]HeldHash
|
|
// Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or
|
|
// not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945).
|
|
Refused []Gap
|
|
Gaps []Gap
|
|
}
|
|
|
|
// HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said or written by
|
|
// the part in shadow.
|
|
type HeldHash struct {
|
|
Hash string `json:"hash"`
|
|
Generation int `json:"generation,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Adopted bool `json:"adopted,omitempty"`
|
|
}
|
|
|
|
// readHashes reads the vault's bus-accounts; a refusal is a gap, and every user then has no hash held.
|
|
func readHashes(r Reader, gaps *[]Gap) map[string]HeldHash {
|
|
out := map[string]HeldHash{}
|
|
for user, raw := range each(r, ReadAccounts, gaps) {
|
|
var h HeldHash
|
|
if json.Unmarshal(raw, &h) != nil || h.Hash == "" {
|
|
*gaps = append(*gaps, Gap{Input: ReadAccounts + " " + user, Why: "holds no hash"})
|
|
continue
|
|
}
|
|
out[user] = h
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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
|
|
}
|
|
// Read for its inputs, as every delivery part reads a manifest (issue 1751): the merge gate judged its config values,
|
|
// and the part reads its seats, buckets, logs and streams, never a value; so a value falling back to a predefined key
|
|
// is taken as it declares itself, where ParseManifest refused it with no list and placed its module nowhere (issue 1945).
|
|
m, err = catalogue.ParseManifestForInputs([]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")
|
|
}
|
|
var m catalogue.Manifest
|
|
if err == nil {
|
|
m, err = cache.get(r, v.Manifest)
|
|
}
|
|
if err != nil {
|
|
// Every manifest, assigned or not: one refused declares no seat, so its holders compose as placed nowhere.
|
|
g := Gap{Input: "manifest of " + module, Why: err.Error()}
|
|
in.Refused = append(in.Refused, g)
|
|
in.Gaps = append(in.Gaps, g)
|
|
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, ", ")})
|
|
}
|
|
in.Hashes = readHashes(r, &in.Gaps)
|
|
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 })
|
|
}
|