Merge pull request 'mesh-grants keeps what the bus loaded while it composes only part of the mesh (issue 1966, bootstrap recovery)' (#171) from fix/1966-grants-keeps-what-is-loaded into main
This commit was merged in pull request #171.
This commit is contained in:
@@ -2414,9 +2414,15 @@ func TestGrants(t *testing.T) {
|
||||
b.loaded = append(b.loaded, grants.User{User: "c.old"})
|
||||
b.put(grants.ReadHave, "b.mod-y", runs)
|
||||
hash("b.mod-y")
|
||||
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "would drop c.old of the list in force") {
|
||||
if res := p.Compose(); res.Sealed == nil || !strings.Contains(res.Sealed.Why, "would take from loaded users: c.old (left out)") ||
|
||||
!slices.Contains(res.KeptLoaded, "c.old") {
|
||||
t.Errorf("a list dropping a loaded user was not held: %+v", res.Sealed)
|
||||
}
|
||||
// With its hash, the loaded user the haves do not compose is kept as the loaded list gives it, and the list sealed.
|
||||
hash("c.old")
|
||||
if res := p.Compose(); res.Sealed == nil || res.Sealed.Why != "" || !strings.Contains(string(b.own[grants.StateUserList][grants.KeyWrittenUsers]), `"c.old"`) {
|
||||
t.Errorf("the loaded user was not kept in the list sealed: %+v", res.Sealed)
|
||||
}
|
||||
// The view answers: the assignments alone places again.
|
||||
delete(b.refused, grants.ReadAssignments)
|
||||
if in := grants.ReadInputs(b, &grants.Manifests{}); in.ByHaves != "" || slices.Contains(in.Assigned["a"], "mesh-delivery-assigner") {
|
||||
@@ -2424,6 +2430,184 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Issue 1966, the list sealed 2026-11-07 23:09: composed from the bootstrapped seats alone, it took the node seats from the
|
||||
// tool runners, the vault's reads from the node-engines, the seat queues from the build modules, and left out the ledger
|
||||
// account of a held module. A pass that composes part of the mesh (the seat list unread, or placed by the haves) seals
|
||||
// each user of the loaded list and of the list in force with the union of what it composes and what those give it, so it
|
||||
// only adds (the indexer's serving of mesh-catalog included); one that would still take a permission is held, naming it.
|
||||
t.Run("a pass composing part of the mesh keeps what the bus loaded, adding only (issue 1966)", func(t *testing.T) {
|
||||
indexer, err := os.ReadFile("../mesh-delivery-indexer/module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const (
|
||||
nodeSeat = "mesh.seat.node-files.call.*.a.*"
|
||||
vaultRead = "$JS.API.DIRECT.GET.KV_mesh-vault_signers.>"
|
||||
buildPull = "$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_BUILD_GO.workers"
|
||||
)
|
||||
ledgerX := strings.Replace(xManifest, `"state"`, `"requires":[{"name":"records","storage":"ledger","template":"ledger",`+
|
||||
`"types":[{"name":"decision","schema":{},"operations":[{"name":"propose","effect":"set-fields"}]}]}],"state"`, 1)
|
||||
catalogSeat := func(s string) bool { return strings.HasPrefix(s, "mesh.seat.mesh-catalog.") }
|
||||
// Review round 1: the list in force gives a wide publish and a deny, the pass composes the user narrower with the
|
||||
// composer's empty deny: the user kept keeps the deny, and dropping it counts as a loss.
|
||||
for _, d := range []struct {
|
||||
name string
|
||||
composed, kept grants.User
|
||||
loss string
|
||||
}{
|
||||
{"a deny of the list in force is kept",
|
||||
grants.User{User: "a.mod-v", Publish: []string{"mesh.a"}, Subscribe: []string{}, PublishDeny: []string{}},
|
||||
grants.User{User: "a.mod-v", Publish: []string{"mesh.>", "mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.seat.mesh-vault.desk.>"}},
|
||||
"deny dropped mesh.seat.mesh-vault.desk.>"},
|
||||
{"a deny the pass composes is kept beside it",
|
||||
grants.User{User: "a.mod-v", Publish: []string{"mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.b"}},
|
||||
grants.User{User: "a.mod-v", Publish: []string{"mesh.>", "mesh.a"}, Subscribe: []string{}, PublishDeny: []string{"mesh.b", "mesh.seat.mesh-vault.desk.>"}},
|
||||
"deny mesh.b"},
|
||||
} {
|
||||
inForce := grants.User{User: "a.mod-v", Publish: []string{"mesh.>"}, Subscribe: []string{}, PublishDeny: []string{"mesh.seat.mesh-vault.desk.>"}}
|
||||
got := grants.KeepLoaded([]grants.User{d.composed}, nil, map[string]grants.User{"a.mod-v": inForce})["a.mod-v"]
|
||||
if !slices.Equal(got.Publish, d.kept.Publish) || !slices.Equal(got.PublishDeny, d.kept.PublishDeny) {
|
||||
t.Errorf("%s: kept %+v, want %+v", d.name, got, d.kept)
|
||||
}
|
||||
if !slices.Contains(grants.Losses(d.composed, inForce), d.loss) {
|
||||
t.Errorf("%s: losses %v name no %q", d.name, grants.Losses(d.composed, inForce), d.loss)
|
||||
}
|
||||
}
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
bend func(*fakeBus, *grants.Part)
|
||||
// held is what the pass's sealed says it holds, "" when it seals.
|
||||
held string
|
||||
}{
|
||||
{"the seat list unread", func(b *fakeBus, p *grants.Part) {
|
||||
p.SeatList = &seatGate{bootstrapped: []string{meshbroker.Seat, meshcatalog.Seat}}
|
||||
}, ""},
|
||||
{"placed by the haves", func(b *fakeBus, p *grants.Part) {
|
||||
b.refused[grants.ReadAssignments] = true
|
||||
runs := nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}
|
||||
b.put(grants.ReadHave, "a.mod-x", runs)
|
||||
b.put(grants.ReadHave, "b.mod-y", runs)
|
||||
b.put(grants.ReadHave, "a.mesh-delivery-indexer", runs)
|
||||
}, ""},
|
||||
// The ledger of a held module stays with its module's users, whatever the seat list (held.go).
|
||||
{"the ledger of a module whose manifest is refused", func(b *fakeBus, p *grants.Part) {
|
||||
b.refuse("mod-x", ledgerX, "it is held")
|
||||
}, ""},
|
||||
// The union cannot give a login a loaded user has no hash for: held, naming it.
|
||||
{"a loaded user with no hash, the seat list unread", func(b *fakeBus, p *grants.Part) {
|
||||
p.SeatList = &seatGate{bootstrapped: []string{meshbroker.Seat, meshcatalog.Seat}}
|
||||
delete(b.states[grants.ReadAccounts], "b.mod-y")
|
||||
}, "would take from loaded users: b.mod-y (left out)"},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.manifestOf("mod-x", ledgerX)
|
||||
b.manifestOf("mesh-delivery-indexer", string(indexer))
|
||||
b.assign("a", "mesh-delivery-indexer")
|
||||
busKey, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
|
||||
UserList: &nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: 8, Wanted: "sha256:aa", Digest: "sha256:aa", Users: 5,
|
||||
Key: busKey.PublicKey, Follows: grantsstate.FollowGrants}})
|
||||
whole := shadowOf(t, b)
|
||||
for _, u := range whole {
|
||||
b.put(grants.ReadAccounts, u.User, map[string]any{"user": u.User, "hash": "$2a$10$x", "generation": 1, "fingerprint": "sha256:1"})
|
||||
}
|
||||
p := partOn(b)
|
||||
p.Objects = &fakeObjects{}
|
||||
// The list in force, composed whole.
|
||||
p.Compose()
|
||||
if g, _, _ := p.Sealed(); g.Digest == "" {
|
||||
t.Fatal("no list in force sealed")
|
||||
}
|
||||
// The indexer's membership as published serves a tool more than any pass composes now (a tool runner's
|
||||
// seat, say): a pass composing part of the mesh keeps it as published, a whole one publishes what it composes.
|
||||
const extra = `{"subject":"mesh.tools.a.mesh-delivery-indexer.extra"}`
|
||||
{
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var e any
|
||||
_ = json.Unmarshal([]byte(extra), &e)
|
||||
m["serves"] = append(m["serves"].([]any), e)
|
||||
b.own[grants.StateMemberships]["a.mesh-delivery-indexer"], _ = json.Marshal(m)
|
||||
}
|
||||
servesExtra := func() bool {
|
||||
return strings.Contains(string(b.own[grants.StateMemberships]["a.mesh-delivery-indexer"]), "mesh-delivery-indexer.extra")
|
||||
}
|
||||
// The bus loaded it with what a whole seat list gives besides, and without the indexer's serving yet.
|
||||
var loaded []grants.User
|
||||
for _, u := range whole {
|
||||
switch u.User {
|
||||
case "a.mod-x":
|
||||
u.Subscribe = append(slices.Clone(u.Subscribe), nodeSeat)
|
||||
case "node.a":
|
||||
u.Subscribe = append(slices.Clone(u.Subscribe), vaultRead)
|
||||
case "b.mod-y":
|
||||
u.Publish = append(slices.Clone(u.Publish), buildPull)
|
||||
case "a.mesh-delivery-indexer":
|
||||
u.Subscribe = slices.DeleteFunc(slices.Clone(u.Subscribe), catalogSeat)
|
||||
}
|
||||
loaded = append(loaded, u)
|
||||
}
|
||||
if !slices.ContainsFunc(loaded, func(u grants.User) bool { return u.User == "ledger.a.mod-x" }) {
|
||||
t.Fatalf("the fixture composes no ledger user: %v", names(loaded))
|
||||
}
|
||||
b.loaded = loaded
|
||||
row.bend(b, p)
|
||||
res := p.Compose()
|
||||
if g, _, _ := p.Sealed(); true {
|
||||
t.Logf("sealed %d users (%s), kept with the loaded list's grants %v, said %q", g.Users, short(g.Digest), res.KeptLoaded, res.Sealed.Why)
|
||||
}
|
||||
partial := row.name != "the ledger of a module whose manifest is refused"
|
||||
if servesExtra() != partial || partial != slices.ContainsFunc(res.NotPublished, func(n string) bool {
|
||||
return strings.HasPrefix(n, "a.mesh-delivery-indexer: kept as published: ") && strings.Contains(n, "mesh-delivery-indexer.extra")
|
||||
}) {
|
||||
t.Errorf("the published membership serving more: kept %v in a pass composing part of the mesh %v; not published %v", servesExtra(), partial, res.NotPublished)
|
||||
}
|
||||
if row.held != "" {
|
||||
if res.Sealed == nil || !strings.Contains(res.Sealed.Why, row.held) {
|
||||
t.Fatalf("not held for %q: %+v", row.held, res.Sealed)
|
||||
}
|
||||
return
|
||||
}
|
||||
g, _, _ := p.Sealed()
|
||||
if res.Sealed == nil || res.Sealed.Why != "" || g.Digest == "" {
|
||||
t.Fatalf("not sealed: %+v (kept %v, refused %v)", res.Sealed, res.KeptLoaded, res.Refused)
|
||||
}
|
||||
sealedAs := func(user string) (grants.User, bool) {
|
||||
raw, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(g.Digest, user))
|
||||
var u grants.User
|
||||
return u, ok && json.Unmarshal(raw, &u) == nil
|
||||
}
|
||||
if row.name == "the ledger of a module whose manifest is refused" {
|
||||
// The seat list read, the pass composes the whole mesh and keeps no loaded permission beyond it; the held
|
||||
// module's users, its ledger's among them, stay as the list in force holds them.
|
||||
if u, ok := sealedAs("ledger.a.mod-x"); !ok || len(grants.Losses(u, whole[slices.IndexFunc(whole, func(u grants.User) bool { return u.User == "ledger.a.mod-x" })])) > 0 {
|
||||
t.Errorf("the held module's ledger account is not kept: %+v (%v)", u, ok)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, l := range loaded {
|
||||
u, ok := sealedAs(l.User)
|
||||
if !ok {
|
||||
t.Errorf("%s of the loaded list is not in the list sealed", l.User)
|
||||
continue
|
||||
}
|
||||
if lost := grants.Losses(u, l); len(lost) > 0 {
|
||||
t.Errorf("%s loses %v in the list sealed", l.User, lost)
|
||||
}
|
||||
}
|
||||
if ix, _ := sealedAs("a.mesh-delivery-indexer"); !slices.ContainsFunc(ix.Subscribe, catalogSeat) {
|
||||
t.Errorf("the indexer does not gain its serving of mesh-catalog: %v", ix.Subscribe)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ADR 0590 clause 5's test for this repository (issue 1126): no module.json here names mesh-controller, the lease
|
||||
// read and the builder's consume of artifacts-gone included. Read as text, as mesh-catalog's controllerdeps check reads
|
||||
// its own: mesh-sdk names no module, so it holds no such check (issue 774, the operator 2026-11-03).
|
||||
@@ -3657,3 +3841,7 @@ func (g *seatGate) MayAct(seats ...string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func short(d string) string {
|
||||
return strings.TrimPrefix(d, "sha256:")[:min(12, len(strings.TrimPrefix(d, "sha256:")))]
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ const HeldModuleCondition = "mesh-grants.module-held"
|
||||
|
||||
// heldModuleUsers are, by user, why it is kept: the users of candidates (the ones the list in force and the bus carry) that
|
||||
// belong to a module placed on a node whose manifest the indexer holds (in.Refused) and whose want names no build. A user of
|
||||
// a module on a node is `<node>.<module>` or `<node>.<module>.<process>` (buscompose.Principal.Username). Pure.
|
||||
// a module on a node is `<node>.<module>`, `<node>.<module>.<process>` or its ledger's `ledger.<node>.<module>`
|
||||
// (buscompose.Principal.Username). Pure.
|
||||
func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
||||
held := map[string]string{}
|
||||
for _, g := range in.Refused {
|
||||
@@ -55,7 +56,8 @@ func heldModuleUsers(in Inputs, candidates []string) map[string]string {
|
||||
state = "unreadable"
|
||||
}
|
||||
for _, user := range candidates {
|
||||
if user == prefix || strings.HasPrefix(user, prefix+".") {
|
||||
// And its ledger account, `ledger.<node>.<module>` (issue 1966: dropped from the list sealed 2026-11-07 23:09).
|
||||
if user == prefix || strings.HasPrefix(user, prefix+".") || user == "ledger."+prefix {
|
||||
out[user] = fmt.Sprintf("the manifest of %s is %s: %s", module, state, why)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,9 @@ type Inputs struct {
|
||||
// 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
|
||||
// SeatListUnread says the seat list has not been read (the gate says so, seatlist.Reader.MayAct with no seat): only the
|
||||
// bootstrapped seats compose, so the pass keeps what the bus loaded (keeploaded.go).
|
||||
SeatListUnread bool
|
||||
// ByHaves says why this pass placed the modules where the node-engines' haves say they run, the assignments view not
|
||||
// answering (bootstrap only, issue 1966); empty when the assignments placed them. Such a pass retires nothing
|
||||
// (ReadFailures) and seals no list dropping a user of the list in force (holdsBack).
|
||||
@@ -321,6 +324,7 @@ 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...)}
|
||||
in.SeatListUnread = gate != nil && !gate.MayAct()
|
||||
nodes := map[string]bool{}
|
||||
// **The seat's assignments alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
|
||||
// assignments are read no more, and the seats held once for the mesh are held where it says. Not read is a failed
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// **A pass that cannot compose the whole mesh keeps what the bus loaded** (issue 1966, the registry delivery's sealed list of
|
||||
// 2026-11-07 23:09). While the seat list is unread (only the bootstrapped seats compose, ADR 0636 §1) or a pass is placed by
|
||||
// the node-engines' haves (Inputs.ByHaves), what the composer derives is a part of the mesh: that list sealed took the node
|
||||
// seats from every tool runner, the vault's reads from the node-engines and the seat queues from the build modules. Such a
|
||||
// pass composes each user of the loaded list and of the list in force as the union of what it composes and what those lists
|
||||
// give it (publish, subscribe, the allowance of responses, and every deny any of them says), so it
|
||||
// only adds, the indexer's serving of the seat that reads the list included; and the list is held where it would still take
|
||||
// a permission from a loaded user (holdsBack, LoadedLosses).
|
||||
|
||||
// keepsLoaded says why this pass keeps what the bus loaded, or "" when it composes the whole mesh.
|
||||
func (in Inputs) keepsLoaded() string {
|
||||
switch {
|
||||
case in.ByHaves != "":
|
||||
return in.ByHaves
|
||||
case in.SeatListUnread:
|
||||
return "the seat list is unread, so only the bootstrapped seats compose"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// KeepLoaded is, by user, the union of each user of loaded and of inForce with what composed gives it, for every such user
|
||||
// composed gives less than either list (or not at all). Pure.
|
||||
func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]User {
|
||||
byUser := map[string]User{}
|
||||
for _, u := range composed {
|
||||
byUser[u.User] = u
|
||||
}
|
||||
others := map[string][]User{}
|
||||
for _, u := range loaded {
|
||||
others[u.User] = append(others[u.User], u)
|
||||
}
|
||||
for name, u := range inForce {
|
||||
others[name] = append(others[name], u)
|
||||
}
|
||||
out := map[string]User{}
|
||||
for name, from := range others {
|
||||
c, composedHere := byUser[name]
|
||||
short := !composedHere
|
||||
for _, o := range from {
|
||||
if len(Losses(c, o)) > 0 {
|
||||
short = true
|
||||
}
|
||||
}
|
||||
if !short {
|
||||
continue
|
||||
}
|
||||
// Every deny any list says is kept: a deny the composer drops while it composes part of the mesh would widen the
|
||||
// user (the composer gives every composed user a deny, empty or not, so an intersection would drop the list in
|
||||
// force's).
|
||||
u := User{User: name, Publish: c.Publish, Subscribe: c.Subscribe, AllowResponses: c.AllowResponses, PublishDeny: c.PublishDeny}
|
||||
for _, o := range from {
|
||||
u.Publish = set(append(slices.Clone(u.Publish), o.Publish...))
|
||||
u.Subscribe = set(append(slices.Clone(u.Subscribe), o.Subscribe...))
|
||||
u.AllowResponses = u.AllowResponses || o.AllowResponses
|
||||
if o.PublishDeny != nil || u.PublishDeny != nil {
|
||||
u.PublishDeny = set(append(slices.Clone(u.PublishDeny), o.PublishDeny...))
|
||||
}
|
||||
}
|
||||
if u.Publish == nil {
|
||||
u.Publish = []string{}
|
||||
}
|
||||
if u.Subscribe == nil {
|
||||
u.Subscribe = []string{}
|
||||
}
|
||||
out[name] = u
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Losses are how now differs from had where had's word is lost: each publish and subscribe had gives that now does not,
|
||||
// the allowance of responses, and, where had's denies are known, each deny now adds and each deny of had now drops (a deny
|
||||
// dropped widens the user past what had held it to). Pure.
|
||||
func Losses(now, had User) []string {
|
||||
var out []string
|
||||
for _, s := range minus(had.Publish, now.Publish) {
|
||||
out = append(out, "publish "+s)
|
||||
}
|
||||
for _, s := range minus(had.Subscribe, now.Subscribe) {
|
||||
out = append(out, "subscribe "+s)
|
||||
}
|
||||
if had.AllowResponses && !now.AllowResponses {
|
||||
out = append(out, "responses")
|
||||
}
|
||||
if had.PublishDeny != nil {
|
||||
for _, s := range minus(now.PublishDeny, had.PublishDeny) {
|
||||
out = append(out, "deny "+s)
|
||||
}
|
||||
for _, s := range minus(had.PublishDeny, now.PublishDeny) {
|
||||
out = append(out, "deny dropped "+s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LoadedLosses names, sorted, each loaded user the list composed now (listed: its users; composed: their grants) would
|
||||
// take a permission from or leave out, with how many it loses. Pure.
|
||||
func LoadedLosses(listed []string, composed, loaded []User) []string {
|
||||
byUser := map[string]User{}
|
||||
for _, u := range composed {
|
||||
byUser[u.User] = u
|
||||
}
|
||||
var out []string
|
||||
for _, l := range loaded {
|
||||
c, ok := byUser[l.User]
|
||||
switch {
|
||||
case !slices.Contains(listed, l.User) || !ok:
|
||||
out = append(out, l.User+" (left out)")
|
||||
default:
|
||||
if n := len(Losses(c, l)); n > 0 {
|
||||
out = append(out, fmt.Sprintf("%s (%d permissions)", l.User, n))
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
|
||||
// MembershipNarrows names what the membership last published (was) gives that the one composed now does not, field by field
|
||||
// (`<field> <value>`), the private network, the contributions received and the bindings aside, which the pass carries over
|
||||
// or the bus module writes; none when now covers was. Both are memberships as JSON. Pure.
|
||||
func MembershipNarrows(was, now []byte) []string {
|
||||
var a, b map[string]any
|
||||
if json.Unmarshal(was, &a) != nil || json.Unmarshal(now, &b) != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, field := range sortedKeys(a) {
|
||||
switch field {
|
||||
case "mesh", "receives", "bindings":
|
||||
continue
|
||||
}
|
||||
out = append(out, uncovered(field, a[field], b[field])...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// uncovered names each part of a that b does not hold: a key of an object by key, an element of a list by its value.
|
||||
func uncovered(path string, a, b any) []string {
|
||||
switch av := a.(type) {
|
||||
case map[string]any:
|
||||
bv, _ := b.(map[string]any)
|
||||
var out []string
|
||||
for _, k := range sortedKeys(av) {
|
||||
out = append(out, uncovered(path+"."+k, av[k], bv[k])...)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
bv, _ := b.([]any)
|
||||
var out []string
|
||||
for _, e := range av {
|
||||
ej, _ := json.Marshal(e)
|
||||
if !slices.ContainsFunc(bv, func(x any) bool { xj, _ := json.Marshal(x); return string(xj) == string(ej) }) {
|
||||
out = append(out, path+" "+string(ej))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
aj, _ := json.Marshal(a)
|
||||
bj, _ := json.Marshal(b)
|
||||
if string(aj) != string(bj) {
|
||||
return []string{path + " " + string(aj)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -105,7 +106,10 @@ type Result struct {
|
||||
Memberships int `json:"memberships"`
|
||||
MembershipsWritten int `json:"memberships_written"`
|
||||
Published []string `json:"published,omitempty"`
|
||||
NotPublished []string `json:"not_published,omitempty"`
|
||||
// KeptLoaded are the users a pass composing part of the mesh gave the union of what it composes and what the loaded list
|
||||
// and the list in force give them (keeploaded.go, issue 1966), read with grants_user_list's last pass.
|
||||
KeptLoaded []string `json:"kept_loaded,omitempty"`
|
||||
NotPublished []string `json:"not_published,omitempty"`
|
||||
// Retired is, by `<node>.<module>`, the seat verbs its last published membership served that mesh-sdk's seat
|
||||
// catalogue no longer has (Membership.Retired): the pass keeps it, since the state memberships holds the bare
|
||||
// membership alone (issue 774).
|
||||
@@ -331,7 +335,27 @@ func (p *Part) Compose() Result {
|
||||
}
|
||||
p.sayPrincipalsRefused(refused, kept)
|
||||
p.sayHeldModules(heldUsers, kept)
|
||||
users = WithHashes(withKept(users, kept), in.Hashes)
|
||||
users = withKept(users, kept)
|
||||
// A pass that composes part of the mesh keeps what the bus loaded and the list in force give each user (keeploaded.go).
|
||||
if why := in.keepsLoaded(); why != "" {
|
||||
if union := KeepLoaded(users, loaded, inForce); len(union) > 0 {
|
||||
merged := make(map[string]string, len(allRefused)+len(union))
|
||||
keptAll := make(map[string]User, len(kept)+len(union))
|
||||
for u, w := range allRefused {
|
||||
merged[u] = w
|
||||
}
|
||||
for u, k := range kept {
|
||||
keptAll[u] = k
|
||||
}
|
||||
for u, k := range union {
|
||||
merged[u], keptAll[u] = "keeps what the loaded list gives it: "+why, k
|
||||
}
|
||||
users = withKept(slices.DeleteFunc(users, func(u User) bool { _, ok := union[u.User]; return ok }), union)
|
||||
allRefused, kept = merged, keptAll
|
||||
res.KeptLoaded = sortedKeys(union)
|
||||
}
|
||||
}
|
||||
users = WithHashes(users, in.Hashes)
|
||||
st.Digest, st.Users = Digest(users), len(users)
|
||||
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
|
||||
if loadErr != nil {
|
||||
@@ -381,7 +405,7 @@ func (p *Part) Compose() Result {
|
||||
bus = names(loaded)
|
||||
lost = holdings.LastHolders(holdings.ByUsers(loaded), holdings.ByUsers(users))
|
||||
}
|
||||
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users)
|
||||
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users, loaded)
|
||||
}
|
||||
ms := Memberships(recs)
|
||||
res.Memberships = len(ms)
|
||||
@@ -410,6 +434,17 @@ func (p *Part) Compose() Result {
|
||||
continue
|
||||
}
|
||||
m, now := publish(was, found, ms[key], holdings, seated)
|
||||
// A pass that composes part of the mesh never narrows a published membership: it keeps the one published
|
||||
// (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it serves.
|
||||
if why := in.keepsLoaded(); now && found && why != "" {
|
||||
if body, err := json.Marshal(m.Membership); err == nil {
|
||||
if bare, err := membership.BareGrantsMembership(was); err == nil {
|
||||
if narrower := MembershipNarrows(bare, body); len(narrower) > 0 {
|
||||
m.Published, now = "kept as published: "+why+", and this pass composes it without "+strings.Join(narrower, ", "), false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
|
||||
// published, so a membership held back never reaches a reader.
|
||||
if now {
|
||||
|
||||
@@ -83,7 +83,7 @@ func (p *Part) Sealed() (grantsstate.GrantsUserList, uint64, bool) {
|
||||
|
||||
// writeSealed puts the list sealed to the bus node and writes the key, each only when what it says changed. It says in
|
||||
// res what it wrote, or why it wrote nothing.
|
||||
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped []string, in Inputs, bus []string, lost []string, composed []User) {
|
||||
func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped []string, in Inputs, bus []string, lost []string, composed, loaded []User) {
|
||||
cur, _, had := p.Sealed()
|
||||
next := cur
|
||||
// The bus node follows this list alone (ADR 0571 clause 8): always since the controller ended by its unassignment
|
||||
@@ -104,10 +104,10 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut, dropped
|
||||
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 cur.Digest != digest && p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), digest) != "":
|
||||
case cur.Digest != digest && p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), composed, loaded, digest) != "":
|
||||
// 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, dropped, bus, lost, listUsers(list), digest)
|
||||
why = p.holdsBack(in, leftOut, dropped, bus, lost, listUsers(list), composed, loaded, digest)
|
||||
held = why
|
||||
p.keepHashHold(res, digest)
|
||||
case cur.Digest != digest || cur.SealedTo != grantsstate.SealingKeyID(key) || cur.Node != in.BusNode:
|
||||
@@ -299,10 +299,10 @@ func listUsers(list []byte) []string {
|
||||
// it kept of it; where it kept none (a list written before this release), the users the bus loaded (bus, nil when
|
||||
// they could not be read). Empty when it may.
|
||||
//
|
||||
// In a pass placed by the node-engines' haves (in.ByHaves, issue 1966) the list is held, too, while it would leave out any user
|
||||
// of the list in force or of the list the bus loaded (listed are the users of the list composed now): a bootstrap placement
|
||||
// adds logins and never takes one away.
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string, digest string) string {
|
||||
// In a pass that composes part of the mesh (placed by the node-engines' haves, or the seat list unread; keeploaded.go, issue
|
||||
// 1966) the list is held, too, while it would leave out any user of the list in force or take any permission from a user of
|
||||
// the list the bus loaded (listed are the users of the list composed now, composed their grants): such a pass only adds.
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string, composed, loaded []User, digest string) string {
|
||||
p.hashHold = nil
|
||||
if f := in.ReadFailed(); len(f) > 0 {
|
||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||
@@ -324,7 +324,10 @@ func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string
|
||||
default:
|
||||
had = bus
|
||||
}
|
||||
if in.ByHaves != "" {
|
||||
if why := in.keepsLoaded(); why != "" {
|
||||
if losses := LoadedLosses(listed, composed, loaded); len(losses) > 0 {
|
||||
return "held: " + why + ", and the list composed so would take from loaded users: " + strings.Join(losses, ", ")
|
||||
}
|
||||
var gone []string
|
||||
for _, u := range append(slices.Clone(had), bus...) {
|
||||
if !slices.Contains(listed, u) && !slices.Contains(gone, u) {
|
||||
@@ -333,7 +336,7 @@ func (p *Part) holdsBack(in Inputs, leftOut, dropped, bus, lost, listed []string
|
||||
}
|
||||
if len(gone) > 0 {
|
||||
slices.Sort(gone)
|
||||
return "held: " + in.ByHaves + ", and the list composed so would drop " + strings.Join(gone, ", ") + " of the list in force"
|
||||
return "held: " + why + ", and the list composed so would drop " + strings.Join(gone, ", ") + " of the list in force"
|
||||
}
|
||||
}
|
||||
var unhashed []string
|
||||
|
||||
Reference in New Issue
Block a user