mesh-grants: say every placed module left without a grant, in the pass and as the condition mesh-grants.grant-missing once the lease is handed away (issue 1126, ADR 0547 §5)
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.
This commit is contained in:
@@ -1435,7 +1435,12 @@ func TestGrants(t *testing.T) {
|
||||
var fresh []asks.Condition
|
||||
fp := partOn(b)
|
||||
fp.Objects = p.Objects
|
||||
fp.Asker, fp.Say = fa, func(c asks.Condition) error { fresh = append(fresh, c); return nil }
|
||||
fp.Asker, fp.Say = fa, func(c asks.Condition) error {
|
||||
if c.Key != grants.GrantMissingCondition { // its own row below
|
||||
fresh = append(fresh, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
fp.Compose()
|
||||
if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != asks.StateCleared {
|
||||
t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh)
|
||||
@@ -1951,6 +1956,52 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Issue 1126 (C5): a module whose source commit has not moved since a build of the old form, with a done key of the
|
||||
// new form at that commit (link2pay, snake, f1-circuits, txt-game), is granted at the indexer's fingerprint, never
|
||||
// judged by its commit; and a placed module left without a grant is said, never passed over: in the pass, and as the
|
||||
// condition mesh-grants.grant-missing once the part's grants are the ones read, cleared when it has one.
|
||||
t.Run("grants: at the indexer's fingerprint whatever the commit, and a placed module without one is said", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error {
|
||||
if c.Key == grants.GrantMissingCondition {
|
||||
said = append(said, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
fp := "src2:d4afe7a1abab5ccdbef6d8629b2245006a2aec60290f430f7b4756e2930ed60c"
|
||||
b.put(grants.ReadModules, "mod-x", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(xManifest)),
|
||||
Repository: "novox/Link2Pay", Commit: "5d20e8bfbc953c51e8ebe3c671fe883df9c36e15"})
|
||||
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, None2: "its builder's version is not read yet",
|
||||
Manifest: delivery.ManifestDigest([]byte(yManifest)), Repository: "novox/y", Commit: "c"})
|
||||
for _, u := range []string{"a.mod-x", "a.nats", "b.mod-y"} {
|
||||
b.put(grants.ReadAccounts, u, map[string]any{"user": u, "hash": "$2a$10$x", "generation": 1})
|
||||
}
|
||||
res := p.Compose()
|
||||
key, _ := delivery.PartGrantKey("mod-x", fp)
|
||||
if _, held := b.own[grants.StateGrants][key]; !held {
|
||||
t.Errorf("mod-x has no grant at the indexer's fingerprint %s: %+v", fp, res.Grants)
|
||||
}
|
||||
if res.Grants == nil || len(res.Grants.Missing) != 1 || !strings.HasPrefix(res.Grants.Missing[0],
|
||||
"mod-y: no fingerprint of the new form: its builder's version is not read yet") {
|
||||
t.Fatalf("missing = %+v", res.Grants)
|
||||
}
|
||||
if len(said) != 1 || said[0].State != asks.StateRaised || !strings.Contains(said[0].Explanation, "mod-y") {
|
||||
t.Fatalf("said %+v", said)
|
||||
}
|
||||
if p.Compose(); len(said) != 1 {
|
||||
t.Errorf("the same missing grant was said again: %+v", said)
|
||||
}
|
||||
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(yManifest)),
|
||||
Repository: "novox/y", Commit: "c"})
|
||||
if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != asks.StateCleared {
|
||||
t.Errorf("once mod-y has a grant: missing %v, said %+v", res.Grants.Missing, said)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC4 (novox/hq ADR 0561 §2): every account the vault mints that the records compose is written to the state
|
||||
// accounts once, with the bus's address and a module's claims, read by the vault alone; a key is deleted when placement
|
||||
// no longer names its user, and never in a pass whose reads failed.
|
||||
@@ -2092,7 +2143,12 @@ func TestGrants(t *testing.T) {
|
||||
// A held consumer that differs is never changed by a pass: it is raised as a condition naming it, and brought to
|
||||
// the composed configuration only by hand (grants_consumer_match), never while the controller holds the lease.
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
||||
p.Say = func(c asks.Condition) error {
|
||||
if c.Key != grants.GrantMissingCondition { // its own row
|
||||
said = append(said, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
made["max_deliver"] = 9
|
||||
res = p.Compose()
|
||||
if len(res.Consumers.Differ) != 1 || !strings.Contains(res.Consumers.Differ[0], "EVENTS.b_mod-c: max deliver 9") ||
|
||||
|
||||
@@ -2,9 +2,11 @@ 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"
|
||||
)
|
||||
@@ -28,6 +30,91 @@ type GrantsWritten struct {
|
||||
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
|
||||
@@ -102,8 +189,9 @@ func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records) {
|
||||
res.Errors = append(res.Errors, "the grants could not be composed: "+err.Error())
|
||||
return
|
||||
}
|
||||
done := &GrantsWritten{Wanted: len(want)}
|
||||
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)
|
||||
|
||||
@@ -118,6 +118,9 @@ type Inputs struct {
|
||||
// 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
|
||||
@@ -241,8 +244,8 @@ func each(r Reader, state string, gaps *[]Gap) map[string][]byte {
|
||||
// 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{},
|
||||
Running: map[string]catalogue.Manifest{},
|
||||
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
|
||||
NoFingerprint: 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
|
||||
@@ -341,8 +344,14 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
if err == nil && v.Manifest == "" {
|
||||
err = fmt.Errorf("the indexer's value names no manifest digest")
|
||||
}
|
||||
if err == nil && v.FP2 != "" {
|
||||
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 {
|
||||
|
||||
@@ -179,6 +179,9 @@ type Part struct {
|
||||
// differSaid is the account consumers' differences last said as a condition, once differKnown (consumers.go).
|
||||
differSaid string
|
||||
differKnown bool
|
||||
// missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go).
|
||||
missingSaid string
|
||||
missingKnown bool
|
||||
// hashHold are the users whose hash changes held the pass's list, or nil (sealed.go).
|
||||
hashHold []string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user