mesh-grants: after take-over the list in force is not replaced by one composed past a failed read or one dropping its users for want of a hash; the switch's effect says when the bus node does not watch the key (review of the switch, issue 1945). Pins mesh-sdk c2a410bb.
This commit is contained in:
@@ -3,7 +3,7 @@ module mesh-grants
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026105311-175a5567b8bd h1:ef6y09cs
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026105311-175a5567b8bd/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee h1:ZdBMkF4WYJi82jb3xYi9Z2lXN/iMTn9r3SFC122ROY4=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026110029-83ee7769d8ee/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -955,6 +955,30 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("effect once the bus node loaded the written list = %s", e)
|
||||
}
|
||||
withKey(loadedBefore)
|
||||
// After take-over a list composed past a failed read, or one dropping a user of the list in force for
|
||||
// want of a hash, is not sealed: the list in force stays, and the pass says why.
|
||||
b.refused[grants.ReadAccounts] = true
|
||||
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a read of this pass failed") {
|
||||
t.Errorf("a pass past a failed read = %+v", res.Sealed)
|
||||
}
|
||||
b.refused[grants.ReadAccounts] = false
|
||||
if now, _, _ := p.Sealed(); now.Digest != g.Digest || now.Object != g.Object {
|
||||
t.Errorf("the list in force moved past a failed read: %s, was %s", now.Digest, g.Digest)
|
||||
}
|
||||
b.mu.Lock()
|
||||
hash := b.states[grants.ReadAccounts]["a.mod-x"]
|
||||
delete(b.states[grants.ReadAccounts], "a.mod-x")
|
||||
b.mu.Unlock()
|
||||
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x would be left out") {
|
||||
t.Errorf("a pass that drops a.mod-x's hash = %+v", res.Sealed)
|
||||
}
|
||||
if now, _, _ := p.Sealed(); now.Digest != g.Digest {
|
||||
t.Errorf("the list in force moved without a.mod-x: %s", now.Digest)
|
||||
}
|
||||
b.put(grants.ReadAccounts, "a.mod-x", json.RawMessage(hash))
|
||||
if p.Compose(); func() bool { now, _, _ := p.Sealed(); return now.Digest != g.Digest }() {
|
||||
t.Errorf("the list in force moved with every read and hash back")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ type Caller interface {
|
||||
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.
|
||||
@@ -193,14 +196,14 @@ func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) {
|
||||
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()})
|
||||
*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()})
|
||||
*gaps = append(*gaps, Gap{Input: state + " " + k, Why: err.Error(), Failed: true})
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
@@ -229,7 +232,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
}
|
||||
haveKeys, err := r.Keys(ReadHave)
|
||||
if err != nil {
|
||||
in.Gaps = append(in.Gaps, Gap{Input: ReadHave, Why: "refused or absent: " + err.Error()})
|
||||
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 {
|
||||
@@ -243,7 +246,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
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()})
|
||||
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
|
||||
@@ -266,11 +269,15 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
// 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()})
|
||||
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 || !found {
|
||||
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)
|
||||
@@ -388,3 +395,17 @@ func gapNamed(gaps []Gap, input string) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package grants
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -89,6 +90,10 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
why = "no node runs the bus's module, so the list is sealed to nobody"
|
||||
case key == "":
|
||||
why = "the bus node's have of the bus module names no key yet (its node-engine says it from this release)"
|
||||
case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut) != "":
|
||||
// After take-over the list in force is never replaced by one composed past a failed read, or one that drops a user
|
||||
// of it for want of a hash: that user's module would be locked out of the bus (review of the switch).
|
||||
why = p.holdsBack(in, leftOut)
|
||||
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
|
||||
object, err := delivery.SealUserList(list, key)
|
||||
if err == nil {
|
||||
@@ -104,7 +109,9 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
}
|
||||
next.Digest, next.Object, next.SealedTo, next.Node = digest, delivery.UserListObjectName(digest), delivery.SealingKeyID(key), in.BusNode
|
||||
}
|
||||
next.Users, next.LeftOut = users, leftOut
|
||||
if next.Digest == digest {
|
||||
next.Users, next.LeftOut = users, leftOut
|
||||
}
|
||||
if next.Digest == "" {
|
||||
next.Users, next.LeftOut = 0, nil
|
||||
}
|
||||
@@ -122,6 +129,9 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
res.Sealed = sealedSays(cur, had, "")
|
||||
return
|
||||
}
|
||||
if next.Digest == digest && cur.Digest != digest {
|
||||
p.keepWrittenUsers(res, list)
|
||||
}
|
||||
// The object the key named before is taken away once the key names the new one: the bucket keeps the list in force.
|
||||
if had && cur.Object != "" && cur.Object != next.Object {
|
||||
if err := p.Objects.DeleteObject(cur.Object); err != nil {
|
||||
@@ -184,6 +194,8 @@ func (p *Part) Effect(state string, loaded *delivery.UserListWritten) string {
|
||||
return "recorded, not in effect: the grants part has written no sealed list for the bus node yet"
|
||||
case state == TookOver && loaded == nil:
|
||||
return "recorded, not in effect: the bus module's have names no loaded list"
|
||||
case state == TookOver && loaded.Unwatched != "" && !fromGrants:
|
||||
return "recorded, not in effect: the bus node does not watch the grants part's key: " + loaded.Unwatched
|
||||
case state == TookOver && loaded.Error != "":
|
||||
return "recorded, not in effect: the bus node could not load the grants part's list: " + loaded.Error
|
||||
case state == TookOver:
|
||||
@@ -229,3 +241,51 @@ func (p *Part) followReasons(res Result, in Inputs) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// KeyWrittenUsers is the key of the state user-list naming the users of the list the key `sealed` names: what a list
|
||||
// composed after take-over must keep.
|
||||
const KeyWrittenUsers = "sealed-users"
|
||||
|
||||
func (p *Part) keepWrittenUsers(res *Result, list []byte) {
|
||||
data, _ := json.Marshal(listUsers(list))
|
||||
if _, err := p.Store.Put(StateUserList, KeyWrittenUsers, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenUsers, err))
|
||||
}
|
||||
}
|
||||
|
||||
var listUser = regexp.MustCompile(`\{\s*user:\s*"([^"]+)"`)
|
||||
|
||||
// listUsers names the users of a list as the composer writes it.
|
||||
func listUsers(list []byte) []string {
|
||||
out := []string{}
|
||||
for _, m := range listUser.FindAllSubmatch(list, -1) {
|
||||
out = append(out, string(m[1]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// holdsBack says why a list composed now may not replace the one in force after take-over: a read failed this pass, or
|
||||
// a user of the list in force is left out for want of a hash. Empty when it may.
|
||||
func (p *Part) holdsBack(in Inputs, leftOut []string) string {
|
||||
if f := in.ReadFailures(); len(f) > 0 {
|
||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||
}
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyWrittenUsers)
|
||||
if err != nil {
|
||||
return "held: the users of the list in force cannot be read: " + err.Error()
|
||||
}
|
||||
var had []string
|
||||
if found {
|
||||
_ = json.Unmarshal(raw, &had)
|
||||
}
|
||||
var lost []string
|
||||
for _, u := range leftOut {
|
||||
if slices.Contains(had, u) {
|
||||
lost = append(lost, u)
|
||||
}
|
||||
}
|
||||
if len(lost) > 0 {
|
||||
return "held: the list in force is kept, since " + strings.Join(lost, ", ") + " would be left out for want of a hash in the vault"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user