Files
mesh-delivery/modules/mesh-grants/internal/grants/inputs.go
T
jschoubben 685810720e mesh-grants: place by the catalogue's placement alone once the controller's lease is handed away, and refuse any other reader of the assignments in this repository (issue 774, ADR 0549 §3, §6)
Why: after the lease write the assignments are frozen, so the part's union kept a module the catalogue takes off a node
with its account, grant, membership and user-list entry. The lease is read as the assigner reads it
(buscompose.LeaseHandedAway); before it, or with no placement on the bus, the union stands. The read stays declared until
the first change after the lease write, named in modules/assignments-readers with the assigner; a TestGrants row refuses
any other module.json that reads mesh-controller.assignments, as mesh-catalog's checks/placement does for the catalogue.
2026-10-28 21:54:47 +01:00

502 lines
20 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 the operator asks on their channel and the part runs on their yes, after its refusals.
package grants
import (
"crypto/sha256"
"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"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
// they hold.
ReadPlacement = delivery.PlacementRead
)
// Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the
// controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write
// they place nothing, and the first change after it takes them out (../assignments-readers names the part until then).
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
ReadAccounts, ReadPlacement, ReadLease}
// 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, CallRootFree}
// 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"`
// Failed says the gap is a read that failed this pass, not one that stands (ADR 0547 clause 7: after take-over a
// list composed past a failed read is not sealed).
Failed bool `json:"failed,omitempty"`
}
// 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 and what the catalogue's placement puts there, sorted: while both
// stand (ADR 0549 §5) a module either names keeps its membership.
Assigned map[string][]string
// Placed is each node's placement as the seat mesh-placement holds it (ADR 0567 §1); empty before any is read.
// Where it is, a seat held once for the mesh is held where placement says, not wherever a module claiming it runs.
Placed map[string]delivery.Placement
// 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
// Fingerprints is the fingerprint of the new form the indexer's trunk value names for each module, by module: what a
// grant is written for (grants.go).
Fingerprints map[string]string
// NoFingerprint is why the indexer's value of a module names no fingerprint of the new form, by module (its none2, or
// that it names none): a module here gets no grant, and that is said (grants.go MissingGrants).
NoFingerprint 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"`
// Fingerprint is the vault's fingerprint of the value the hash is of: with the generation, what says the password
// changed, never the hash itself (a hash of the same password differs by its salt).
Fingerprint string `json:"fingerprint,omitempty"`
// Rotating is the vault's rotation of the account under way: a change of its hash it announces (ADR 0561 §4, §7).
Rotating *struct {
Login string `json:"login"`
} `json:"rotating,omitempty"`
}
// credentialOf is what says which password a held hash is of, never the hash: adopted, or the vault's generation and the
// fingerprint of its value.
func credentialOf(h HeldHash) string {
if h.Adopted {
return "adopted"
}
return fmt.Sprintf("generation %d %s", h.Generation, h.Fingerprint)
}
// 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(), Failed: true})
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(), Failed: true})
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{}, Fingerprints: map[string]string{},
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
nodes := map[string]bool{}
// **After the lease write the assignments place nothing** (novox/hq ADR 0549 §3, §6): they are frozen then, so a
// module the catalogue takes off a node would keep its account, grant, membership and user-list entry. Once the lease
// is handed away and the placement is read, the placement alone places modules, as the assigner's Placed does with
// the same lease reading (buscompose.LeaseHandedAway); before, or with no placement on the bus, the two are unioned.
byAssignment, busByAssignment := map[string][]string{}, ""
for _, raw := range each(r, ReadAssignments, &in.Gaps) {
var a assignment
if json.Unmarshal(raw, &a) != nil || !a.stands() || a.Node == "" || a.Module == "" {
continue
}
byAssignment[a.Node] = append(byAssignment[a.Node], a.Module)
if a.Module == BusModule {
busByAssignment = a.Node
}
}
// The seat's placement (ADR 0567 §9): what it places is composed beside the assignments, and the seats held once
// for the mesh are held where it says. Not on the bus yet is a standing gap, never a failed read, so the list is still
// composed and sealed from the assignments alone.
if keys, err := r.Keys(ReadPlacement); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "not read yet: the assignments alone place modules (" + err.Error() + ")"})
} else {
for _, k := range keys {
raw, found, err := r.Get(ReadPlacement, k)
if err != nil || !found {
continue
}
p, err := delivery.DecodePlacement(raw)
node, kerr := delivery.ParsePlacementKey(k)
if err != nil || kerr != nil || p.Node != node {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement + " " + k, Why: "not a placement, so the node is composed from its assignments alone"})
continue
}
if in.Placed == nil {
in.Placed = map[string]delivery.Placement{}
}
in.Placed[node] = p
}
}
away, _ := leaseHandedAway(r)
if away && len(in.Placed) > 0 {
in.Gaps = append(in.Gaps, Gap{Input: ReadAssignments, Why: "not used: the controller's lease is handed away, " +
"so the catalogue's placement alone places modules (ADR 0549 §3, §6)"})
} else {
for node, ms := range byAssignment {
nodes[node] = true
in.Assigned[node] = append(in.Assigned[node], ms...)
}
in.BusNode = busByAssignment
}
for node, p := range in.Placed {
nodes[node] = true
for _, m := range p.Assigned() {
if !slices.Contains(in.Assigned[node], m) {
in.Assigned[node] = append(in.Assigned[node], m)
}
if m == BusModule && in.BusNode == "" {
in.BusNode = node
}
}
}
if len(in.Placed) > 0 {
in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "seat holdings" })
}
haveKeys, err := r.Keys(ReadHave)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadHave, Why: "refused or absent: " + err.Error(), Failed: true})
}
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(), Failed: true})
} 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(), Failed: true})
}
for _, module := range indexed {
raw, found, err := r.Get(ReadModules, module)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadModules + " " + module, Why: err.Error(), Failed: true})
continue
}
if !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 && strings.HasPrefix(v.FP2, delivery.SourcePrefix2) {
in.Fingerprints[module] = v.FP2
} else if err == nil {
why := v.None2
if why == "" {
why = "the indexer's value names no fingerprint of the new form"
}
in.NoFingerprint[module] = why
}
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
}
// A module the mesh provides itself has no repository the indexer reads: its manifest comes with mesh-sdk, and the
// controller's Provide overwrites any row of it at every start, so the provided one always wins here too (issue 1945,
// the private network on every machine). An indexer manifest of the same name is said as a gap, never used.
for _, name := range delivery.ProvidedModules() {
if !slices.Contains(modules, name) {
continue
}
m, raw, err := delivery.ProvidedManifest(name)
in.Gaps = slices.DeleteFunc(in.Gaps, func(g Gap) bool { return g.Input == "manifest of "+name })
in.Refused = slices.DeleteFunc(in.Refused, func(g Gap) bool { return g.Input == "manifest of "+name })
if _, held := in.Digests[name]; held {
in.Gaps = append(in.Gaps, Gap{Input: "manifest of " + name, Why: "the indexer also holds one, which is not used: the mesh provides this module's manifest itself, and the controller overwrites any row of it"})
}
if err != nil {
g := Gap{Input: "manifest of " + name, Why: err.Error()}
in.Refused = append(in.Refused, g)
in.Gaps = append(in.Gaps, g)
delete(in.Manifests, name)
delete(in.Digests, name)
} else {
in.Manifests[name] = m
in.Digests[name] = fmt.Sprintf("provided:sha256:%x", sha256.Sum256(raw))
}
}
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 })
}
// ReadFailures names every read this pass could not make and every manifest refused: what a list composed now may lack.
func (in Inputs) ReadFailures() []string {
var out []string
for _, g := range in.Gaps {
if g.Failed {
out = append(out, g.Input+": "+g.Why)
}
}
for _, g := range in.Refused {
out = append(out, g.Input+": "+g.Why)
}
return out
}