mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/delivery-group group fix/1945-credential-when-row-without-seal held: a member is held: novox/mesh-controller@fbf14b68dbab (merged without a passing check:…
mesh/check checking fbf14b68dbab on 40034be479f8
Migration 0108 marks every standing account sealed, ace's stale mesh-build-go rows from 17:19 among them, and the forgetting ran only at a later unassign: the next assignment to ace would have issued nothing again. - The start's look first forgets, from each node's own resolution (assigned and required), the accounts of every module that no longer resolves on its node, and says each; only from a reading that holds assignments, and a node whose resolution is unknown is left alone. - Unassign forgets a module's accounts only once it no longer resolves on the node (a module another requires keeps running), and never the node's own provisioner account; the forgetting moved from the inventory to the act. - Forgetting holds each account's lock across its delete, so an issue in flight never writes a row back after it. - A module whose accounts are not all sealed is granted its sealed accounts alone, and its grant is dropped when none is: an older grant naming ace no longer lets the assigner want it. - An issue's locked section has a deadline of two minutes, since it holds a connection of the store's pool across the vault and the bus. Tests: cases in the existing tables. Research 072: the grants piece (memberships and credentials heard from placement).
289 lines
11 KiB
Go
289 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/builderkeys"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// The grants issued (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6, issue 1394): what the assigner's sixth rule reads,
|
|
// that a module's bus credential for a fingerprint exists. A module's permissions on the bus are what the bus's user list
|
|
// composes from its registered manifest (inv.BusRecords), and a module's manifest is registered at a fingerprint when the
|
|
// controller takes it in from the builder's key; so once the list is sent to the machine that runs the bus, the
|
|
// credential of every module it carried is issued at the fingerprint of the build it is registered at. The controller
|
|
// writes `<module>.<fingerprint hex>` then, naming the accounts it composed for the module, never removed. A module one
|
|
// of whose accounts has no password yet (the list leaves it out) has no grant until a later send carries it.
|
|
|
|
// grantsFrom is the grant of every module at the fingerprint current names for it, naming its accounts that have a
|
|
// sealed password in hashes; a module whose current build names no fingerprint of the new form has none. A module one
|
|
// of whose accounts has none is granted its sealed accounts alone, and when it has none, its grant is dropped (drop):
|
|
// an older grant naming a node whose account is gone would let the assigner want it (issue 1945, rule 6 per node), and
|
|
// an empty grant names every node. principals are the bus's users as broker.Users composes them.
|
|
func grantsFrom(principals []broker.Principal, hashes map[string]string, current map[string]string) (
|
|
map[string]sdkdelivery.ControllerGrant, []string) {
|
|
accounts, missing := map[string][]string{}, map[string]bool{}
|
|
for _, p := range principals {
|
|
if p.Module == "" || (p.Kind != broker.KindModule && p.Kind != broker.KindProcess) {
|
|
continue
|
|
}
|
|
if hashes[p.Username()] == "" {
|
|
missing[p.Module] = true
|
|
continue
|
|
}
|
|
accounts[p.Module] = append(accounts[p.Module], p.Username())
|
|
}
|
|
out := map[string]sdkdelivery.ControllerGrant{}
|
|
var drop []string
|
|
for module, fp := range current {
|
|
if !strings.HasPrefix(fp, sdkdelivery.SourcePrefix2) {
|
|
continue
|
|
}
|
|
key, err := sdkdelivery.ControllerGrantKey(module, fp)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
names := accounts[module]
|
|
if missing[module] && len(names) == 0 {
|
|
drop = append(drop, key)
|
|
continue
|
|
}
|
|
sort.Strings(names)
|
|
out[key] = sdkdelivery.ControllerGrant{Accounts: names}
|
|
}
|
|
sort.Strings(drop)
|
|
return out, drop
|
|
}
|
|
|
|
// currentFingerprints is, by module, the fingerprint of the build each registered module is registered at: its
|
|
// manifest's commit (built_from), that commit's newest successful build, and the fingerprint that build names.
|
|
//
|
|
// **A commit with no build of its own asks the indexer** (issue 1945, live from 2026-10-25 12:43). A catalogue merge
|
|
// that leaves a module's sources alone re-indexes it at the merge's commit with the fingerprint it had: the take-in
|
|
// registers the module at the new commit from the same done key, whose build record (one per key and revision) already
|
|
// stands at the commit it was first taken in at, so the new commit has no build pointing at it. Read only by the
|
|
// commit, such a module had no fingerprint of the new form: it was composed into the node's own composition while its
|
|
// want named its own, and every node declared its files twice. The indexer's trunk value at the commit names the
|
|
// fingerprint the module was registered from, so that is its fingerprint.
|
|
func currentFingerprints(ctx context.Context, inv *inventory.Inventory) (map[string]string, error) {
|
|
builds, err := inv.CurrentBuilds(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commits := make(map[string]string, len(builds))
|
|
for module, b := range builds {
|
|
commits[module] = b.Commit
|
|
}
|
|
var indexed func(ctx context.Context, module, commit string) (string, error)
|
|
var closeIndexed func()
|
|
defer func() {
|
|
if closeIndexed != nil {
|
|
closeIndexed()
|
|
}
|
|
}()
|
|
return fingerprintsAt(ctx, commits,
|
|
func(ctx context.Context, module, commit string) (string, bool, error) {
|
|
id, err := inv.BuildOf(ctx, module, commit)
|
|
if err != nil || id == "" {
|
|
return "", false, nil
|
|
}
|
|
build, found, err := inv.BuildByID(ctx, id)
|
|
if err != nil || !found {
|
|
return "", false, nil
|
|
}
|
|
return build.SourceFingerprint, true, nil
|
|
},
|
|
func(ctx context.Context, module, commit string) (string, error) {
|
|
if indexed == nil {
|
|
read, done, err := openIndexedFingerprints()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
indexed, closeIndexed = read, done
|
|
}
|
|
return indexed(ctx, module, commit)
|
|
})
|
|
}
|
|
|
|
// fingerprintsAt is, by module, the fingerprint of the build registered at its commit (built), or else the fingerprint
|
|
// of the new form the indexer's trunk value at that commit names (indexed). A module with no commit (held without a
|
|
// source) has none, and a module neither knows has none. An error of the indexer's is the answer's: a module left out
|
|
// for it would be composed into the node's own composition beside its own (issue 1945).
|
|
func fingerprintsAt(ctx context.Context, commits map[string]string,
|
|
built func(ctx context.Context, module, commit string) (string, bool, error),
|
|
indexed func(ctx context.Context, module, commit string) (string, error)) (map[string]string, error) {
|
|
modules := make([]string, 0, len(commits))
|
|
for module := range commits {
|
|
modules = append(modules, module)
|
|
}
|
|
sort.Strings(modules)
|
|
out := map[string]string{}
|
|
for _, module := range modules {
|
|
commit := commits[module]
|
|
if commit == "" {
|
|
continue
|
|
}
|
|
fp, found, err := built(ctx, module, commit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if found {
|
|
out[module] = fp
|
|
continue
|
|
}
|
|
fp, err = indexed(ctx, module, commit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the fingerprint %s is registered at (%.8s): %w", module, commit, err)
|
|
}
|
|
if strings.HasPrefix(fp, sdkdelivery.SourcePrefix2) {
|
|
out[module] = fp
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// openIndexedFingerprints opens the indexer's trunk values: what reads the fingerprint of the new form a module's value
|
|
// at a commit names ("" when it holds none for that commit), and what closes the bus connection it opened.
|
|
var openIndexedFingerprints = func() (func(ctx context.Context, module, commit string) (string, error), func(), error) {
|
|
keys, closeKeys, err := openBuilderKeys()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("the indexer's trunk values: %w", err)
|
|
}
|
|
return func(ctx context.Context, module, commit string) (string, error) {
|
|
return indexedFingerprintAt(ctx, keys, module, commit)
|
|
}, closeKeys, nil
|
|
}
|
|
|
|
// indexedFingerprintAt is the fingerprint of the new form the indexer's value of module at commit names: the value it
|
|
// holds now when that is the commit's (one read), else the newest of the key's history at the commit; "" when none.
|
|
func indexedFingerprintAt(ctx context.Context, keys interface {
|
|
ModuleAt(ctx context.Context, module, commit string) (sdkdelivery.Module, error)
|
|
}, module, commit string) (string, error) {
|
|
m, err := keys.ModuleAt(ctx, module, "")
|
|
if err == nil && sameCommitID(m.Commit, commit) {
|
|
return m.FP2, nil
|
|
}
|
|
if err != nil && !errors.Is(err, builderkeys.ErrNoValue) {
|
|
return "", err
|
|
}
|
|
if errors.Is(err, builderkeys.ErrNoValue) {
|
|
return "", nil
|
|
}
|
|
m, err = keys.ModuleAt(ctx, module, commit)
|
|
if errors.Is(err, builderkeys.ErrNoValue) {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return m.FP2, nil
|
|
}
|
|
|
|
// grantsKV is what writing the grants needs of their bucket.
|
|
type grantsKV interface {
|
|
Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error)
|
|
Create(ctx context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt) (uint64, error)
|
|
Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error)
|
|
}
|
|
|
|
// keepGrants writes every grant the list just sent issued that the bucket does not hold as it is: a create for a new one,
|
|
// a compare-and-set for one whose accounts changed. It answers how many it wrote.
|
|
func keepGrants(ctx context.Context, kv grantsKV, grants map[string]sdkdelivery.ControllerGrant) (int, error) {
|
|
keys := make([]string, 0, len(grants))
|
|
for k := range grants {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
wrote := 0
|
|
var errs []error
|
|
for _, key := range keys {
|
|
value, err := sdkdelivery.EncodeControllerGrant(grants[key])
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", key, err))
|
|
continue
|
|
}
|
|
e, err := kv.Get(ctx, key)
|
|
switch {
|
|
case errors.Is(err, jetstream.ErrKeyNotFound):
|
|
_, err = kv.Create(ctx, key, value)
|
|
case err != nil:
|
|
default:
|
|
if before, derr := sdkdelivery.DecodeControllerGrant(e.Value()); derr == nil && sdkdelivery.SameControllerGrant(before, withVersion(grants[key])) {
|
|
continue
|
|
}
|
|
_, err = kv.Update(ctx, key, value, e.Revision())
|
|
}
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", key, err))
|
|
continue
|
|
}
|
|
wrote++
|
|
}
|
|
return wrote, errors.Join(errs...)
|
|
}
|
|
|
|
func withVersion(g sdkdelivery.ControllerGrant) sdkdelivery.ControllerGrant {
|
|
g.V = sdkdelivery.Version
|
|
return g
|
|
}
|
|
|
|
// grantsIssued writes the grants issued for every module whose credentials all exist at the fingerprint it is
|
|
// registered at (ADR 0528 clause 2, ADR 0539 clause 7). Its error is the caller's: with no send left, nothing writes a
|
|
// missed grant later but the caller's retry (the done key's take-in, composed_keys.go), and the assigner's wants wait.
|
|
func grantsIssued(ctx context.Context, inv *inventory.Inventory, js jetstream.JetStream, indent string) error {
|
|
kv, err := js.KeyValue(ctx, broker.GrantsBucket)
|
|
if err != nil {
|
|
return fmt.Errorf("the grants issued are not written: the bucket %s: %w", broker.GrantsBucket, err)
|
|
}
|
|
records, err := inv.BusRecords(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("the grants issued are not written: %w", err)
|
|
}
|
|
principals, err := broker.Users(records)
|
|
if err != nil {
|
|
return fmt.Errorf("the grants issued are not written: %w", err)
|
|
}
|
|
kept, err := inv.BusUsers(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("the grants issued are not written: %w", err)
|
|
}
|
|
hashes := make(map[string]string, len(kept))
|
|
for name, u := range kept {
|
|
// A grant names an account whose credential is sealed to its node, never one only minted: the assigner wants a
|
|
// node only once the grant names its account (issue 1945, rule 6 per node).
|
|
if u.Sealed {
|
|
hashes[name] = u.PasswordHash
|
|
}
|
|
}
|
|
current, err := currentFingerprints(ctx, inv)
|
|
if err != nil {
|
|
return fmt.Errorf("the grants issued are not written: %w", err)
|
|
}
|
|
grants, drop := grantsFrom(principals, hashes, current)
|
|
wrote, err := keepGrants(ctx, kv, grants)
|
|
for _, key := range drop {
|
|
if _, gerr := kv.Get(ctx, key); gerr == nil {
|
|
if derr := kv.Delete(ctx, key); derr != nil {
|
|
err = errors.Join(err, fmt.Errorf("%s: %w", key, derr))
|
|
} else {
|
|
fmt.Printf("%sthe grant %s dropped: none of its accounts is sealed to its node\n", indent, key)
|
|
}
|
|
}
|
|
}
|
|
if wrote > 0 {
|
|
fmt.Printf("%s%d grant(s) issued written for the assigner\n", indent, wrote)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("some grants issued are not written: %w", err)
|
|
}
|
|
return nil
|
|
}
|