Why: the controller's grant writer skipped link2pay, snake, f1-circuits and txt-game in silence (it read the fingerprint by their unchanged commit, a build of the old form) and the take-in said 'its grant written'. The part already reads the fingerprint from the indexer's value, so those four are granted at their src2 done key's fingerprint; a row proves it. What a pass leaves without a grant (no fingerprint of the new form with the indexer's none2, a refused manifest, no hashed account) is now said rather than passed over. Quick: log it. Thorough (chosen): the pass's Missing, shown by the grants tool, and a warning condition raised and cleared on change, as consumer-differs is.
269 lines
10 KiB
Go
269 lines
10 KiB
Go
package grants
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **The grants the part writes** (novox/hq ADR 0547 §4, §5, ADR 0561 §2, §6; issue 774). A module's grant is the record
|
|
// that its bus credential exists for the fingerprint the catalogue's trunk names for it: `<module>.<fingerprint hex>` in
|
|
// the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the
|
|
// controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key.
|
|
// Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's
|
|
// (delivery.GrantSource), and the tool grants says every difference, so the switch is made on a measured equality.
|
|
//
|
|
// A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has
|
|
// one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the
|
|
// assigner want it, and a grant naming no account names every node). Nothing else is ever deleted, and nothing is written
|
|
// or deleted in a pass that could not read the vault's hashes, the indexer's modules or a manifest of the module.
|
|
|
|
// GrantsWritten is what one pass did to the state grants.
|
|
type GrantsWritten struct {
|
|
Wanted int `json:"wanted"`
|
|
Written []string `json:"written,omitempty"`
|
|
Deleted []string `json:"deleted,omitempty"`
|
|
// Kept says why a pass wrote and deleted nothing, or left a module out.
|
|
Kept []string `json:"kept,omitempty"`
|
|
// Missing is each module the placement or an assignment puts on a machine that has no grant after the pass,
|
|
// `<module>: <why>`: the assigner's sixth rule wants none of it (MissingGrants). Never passed over in silence: once
|
|
// the part's grants are the ones read, it is the condition mesh-grants.grant-missing.
|
|
Missing []string `json:"missing,omitempty"`
|
|
}
|
|
|
|
// GrantMissingCondition is the key of the warning raised while a placed module has no grant (issue 1126).
|
|
const GrantMissingCondition = "mesh-grants.grant-missing"
|
|
|
|
// MissingGrants is each module assigned or placed on a machine that want leaves without a grant, `<module>: <why>`,
|
|
// sorted: its manifest refused (skip), no fingerprint of the new form at the indexer's trunk, or no account with a hash
|
|
// in the vault (drop). **The fingerprint is the indexer's, never a commit's** (issue 1126: link2pay, snake, f1-circuits
|
|
// and txt-game, whose source commit has not moved since a build of the old form, have a done key of the new form at it,
|
|
// and that is the fingerprint granted). Pure.
|
|
func MissingGrants(in Inputs, want map[string]delivery.ControllerGrant, drop []string, skip map[string]bool) []string {
|
|
placed := map[string]bool{}
|
|
for _, ms := range in.Assigned {
|
|
for _, m := range ms {
|
|
placed[m] = true
|
|
}
|
|
}
|
|
var out []string
|
|
for _, module := range sortedKeys(placed) {
|
|
fp, has := in.Fingerprints[module]
|
|
key, err := delivery.PartGrantKey(module, fp)
|
|
if has && err == nil {
|
|
if _, granted := want[key]; granted {
|
|
continue
|
|
}
|
|
}
|
|
why := ""
|
|
switch {
|
|
case skip[module]:
|
|
why = "its manifest at the indexer's trunk is refused"
|
|
case !has:
|
|
why = "no fingerprint of the new form: " + orSaid(in.NoFingerprint[module], "the indexer holds no value of it")
|
|
case err != nil:
|
|
why = fmt.Sprintf("no grant key for %s: %v", fp, err)
|
|
case slices.Contains(drop, key):
|
|
why = "none of its accounts at " + fp + " has a hash in the vault"
|
|
default:
|
|
why = "no grant composed at " + fp
|
|
}
|
|
out = append(out, module+": "+why)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func orSaid(s, none string) string {
|
|
if s == "" {
|
|
return none
|
|
}
|
|
return s
|
|
}
|
|
|
|
// sayMissing raises mesh-grants.grant-missing while a placed module has no grant, once the controller's lease is handed
|
|
// away (the part's grants are then the ones the assigner reads), and clears it when every one has one; in shadow the
|
|
// pass and the tool say them. Said once per set, again when it changes, and always at the first pass after a start.
|
|
func (p *Part) sayMissing(missing []string) {
|
|
if p.Say == nil {
|
|
return
|
|
}
|
|
why := ""
|
|
if on, _ := leaseHandedAway(p.Read); on && len(missing) > 0 {
|
|
why = strings.Join(missing, "; ")
|
|
}
|
|
p.holdMu.Lock()
|
|
defer p.holdMu.Unlock()
|
|
if p.missingKnown && why == p.missingSaid {
|
|
return
|
|
}
|
|
c := asks.Condition{Key: GrantMissingCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
|
if why == "" {
|
|
c.State = asks.StateCleared
|
|
} else {
|
|
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
|
c.Summary = fmt.Sprintf("%d placed module(s) have no grant, so no want can name them", len(missing))
|
|
c.Headline, c.Needs = "A placed module has no bus grant", "a look at why each has none"
|
|
c.Explanation = asks.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " +
|
|
"credential rule gives them no want: " + why
|
|
c.Resolved = "every placed module has a grant at the fingerprint the indexer names"
|
|
}
|
|
if p.Say(c) == nil {
|
|
p.missingSaid, p.missingKnown = why, true
|
|
}
|
|
}
|
|
|
|
// GrantsOf is the grant of every module at the fingerprint the indexer's trunk names for it, by key, and the keys that
|
|
// have none (drop): see the comment above. A module in skip (its manifest was refused) is neither. Pure.
|
|
func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[string]delivery.ControllerGrant, []string, error) {
|
|
principals, err := buscompose.Users(recs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
accounts, missing := map[string][]string{}, map[string]bool{}
|
|
for _, p := range principals {
|
|
if p.Module == "" || (p.Kind != buscompose.KindModule && p.Kind != buscompose.KindProcess) {
|
|
continue
|
|
}
|
|
if in.Hashes[p.Username()].Hash == "" {
|
|
missing[p.Module] = true
|
|
continue
|
|
}
|
|
accounts[p.Module] = append(accounts[p.Module], p.Username())
|
|
}
|
|
out := map[string]delivery.ControllerGrant{}
|
|
var drop []string
|
|
for module, fp := range in.Fingerprints {
|
|
if skip[module] || !strings.HasPrefix(fp, delivery.SourcePrefix2) {
|
|
continue
|
|
}
|
|
key, err := delivery.PartGrantKey(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] = delivery.ControllerGrant{Accounts: names}
|
|
}
|
|
sort.Strings(drop)
|
|
return out, drop, nil
|
|
}
|
|
|
|
// grantsReadFailed names a read of this pass without which no grant is written: the hashes and the indexer's modules and
|
|
// manifests. A manifest refused for one module only leaves that module out (writeGrants).
|
|
func grantsReadFailed(in Inputs) string {
|
|
for _, g := range in.Gaps {
|
|
if !g.Failed {
|
|
continue
|
|
}
|
|
for _, input := range []string{ReadAccounts, ReadModules, ReadManifests} {
|
|
if strings.HasPrefix(g.Input, input) {
|
|
return g.Input + ": " + g.Why
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// writeGrants writes each grant whose key is absent or differs and says grant-written for each, and deletes the key of a
|
|
// module that has none now.
|
|
func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records) {
|
|
if why := grantsReadFailed(in); why != "" {
|
|
res.Grants = &GrantsWritten{Kept: []string{"no grant is written or deleted in a pass that could not read " + why}}
|
|
return
|
|
}
|
|
skip := map[string]bool{}
|
|
for _, g := range in.Refused {
|
|
skip[strings.TrimPrefix(g.Input, "manifest of ")] = true
|
|
}
|
|
want, drop, err := GrantsOf(in, recs, skip)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, "the grants could not be composed: "+err.Error())
|
|
return
|
|
}
|
|
done := &GrantsWritten{Wanted: len(want), Missing: MissingGrants(in, want, drop, skip)}
|
|
res.Grants = done
|
|
p.sayMissing(done.Missing)
|
|
for _, key := range sortedKeys(want) {
|
|
g := want[key]
|
|
data, err := delivery.EncodeControllerGrant(g)
|
|
if err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("the grant %s cannot be encoded: %v", key, err))
|
|
continue
|
|
}
|
|
if raw, _, found, err := p.Store.Get(StateGrants, key); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateGrants, key, err))
|
|
continue
|
|
} else if found {
|
|
if was, err := delivery.DecodeControllerGrant(raw); err == nil && delivery.SameControllerGrant(was, withVersion(g)) {
|
|
continue
|
|
}
|
|
}
|
|
if _, err := p.Store.Put(StateGrants, key, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateGrants, key, err))
|
|
continue
|
|
}
|
|
done.Written = append(done.Written, key)
|
|
module, fp, _ := delivery.ParsePartGrantKey(key)
|
|
p.say(res, EventGrantWritten, map[string]any{"module": module, "fingerprint": fp, "accounts": g.Accounts})
|
|
}
|
|
for _, key := range drop {
|
|
if _, _, found, err := p.Store.Get(StateGrants, key); err != nil || !found {
|
|
continue
|
|
}
|
|
if err := p.Store.Delete(StateGrants, key); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be deleted: %v", StateGrants, key, err))
|
|
continue
|
|
}
|
|
done.Deleted = append(done.Deleted, key)
|
|
}
|
|
}
|
|
|
|
func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant {
|
|
g.V = delivery.Version
|
|
return g
|
|
}
|
|
|
|
// GrantDifference is one key on which the part's grants and the controller's differ.
|
|
type GrantDifference struct {
|
|
Key string `json:"key"`
|
|
// Kind is `only the controller's`, `only the part's` or `accounts differ`.
|
|
Kind string `json:"kind"`
|
|
Controller []string `json:"controller_accounts,omitempty"`
|
|
Part []string `json:"part_accounts,omitempty"`
|
|
}
|
|
|
|
// CompareGrants lists every key the two sets differ on, in key order: a key held by one alone, or held by both with other
|
|
// accounts. Pure.
|
|
func CompareGrants(controller, part map[string]delivery.ControllerGrant) []GrantDifference {
|
|
keys := map[string]bool{}
|
|
for k := range controller {
|
|
keys[k] = true
|
|
}
|
|
for k := range part {
|
|
keys[k] = true
|
|
}
|
|
out := []GrantDifference{}
|
|
for _, k := range sortedKeys(keys) {
|
|
c, inC := controller[k]
|
|
g, inP := part[k]
|
|
switch {
|
|
case inC && !inP:
|
|
out = append(out, GrantDifference{Key: k, Kind: "only the controller's", Controller: c.Accounts})
|
|
case inP && !inC:
|
|
out = append(out, GrantDifference{Key: k, Kind: "only the part's", Part: g.Accounts})
|
|
case !delivery.SameControllerGrant(withVersion(c), withVersion(g)):
|
|
out = append(out, GrantDifference{Key: k, Kind: "accounts differ", Controller: c.Accounts, Part: g.Accounts})
|
|
}
|
|
}
|
|
return out
|
|
}
|