diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 93d9d03..fc070dd 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030143623-83cf97b690f5 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index 05084f7..c582b63 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030143623-83cf97b690f5 h1:hyyaB1rU7uzlQZvWAxZ9P6pL/4LCFpOVXMRgn9F7fic= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030143623-83cf97b690f5/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= diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 7a9725e..add0e27 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -1,6 +1,7 @@ package manifest import ( + "bytes" "context" "encoding/json" "errors" @@ -9,6 +10,7 @@ import ( "maps" "os" "path/filepath" + "reflect" "slices" "strings" "sync" @@ -23,6 +25,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" + "git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/outward" "git.novox.be/novox/mesh-sdk/go/seal" @@ -1296,7 +1299,7 @@ func TestGrants(t *testing.T) { } ms, _ := p.MembershipOf("a", "mod-x") - if mo, _ := ms.(map[string]any); !strings.Contains(fmt.Sprint(mo["published"]), "last_pass") || mo["composed"] == nil { + if mo, _ := ms.(map[string]any); !strings.Contains(fmt.Sprint(mo["published"]), "membership:") || mo["last_pass"] == nil || mo["composed"] == nil { t.Errorf("membership = %v", ms) } gr, _ := p.Grants("mod-x") @@ -1727,7 +1730,7 @@ func TestGrants(t *testing.T) { return m }, true, "", ""}, {"a change that drops verified-sender its holder still asks for is not published, and named", verified, false, "channel telegram verified-sender of a/mod-x", telegramClaim}, - {"a change that drops verified-sender its holder no longer asks for is published", verified, true, `"published":"published"`, `[{"name":"channel","scope":"mesh","kind":"telegram"}]`}, + {"a change that drops verified-sender its holder no longer asks for is published", verified, true, "published:published", `[{"name":"channel","scope":"mesh","kind":"telegram"}]`}, {"a change that takes the last holder from a seat a placed module claims is not published, the seat named", func(m map[string]any) map[string]any { m["seats"] = []any{map[string]any{"seat": brokerName, "verb": brokerVerb, "subject": "mesh.seat." + brokerName + ".tool." + brokerVerb}} return m @@ -1755,25 +1758,38 @@ func TestGrants(t *testing.T) { b.stream = map[string][]byte{"mesh.assignment." + key: raw} b.seqs = map[string]uint64{"mesh.assignment." + key: 4} } - res := partOn(b).Compose() + res := partOn(b).Pass() if got := slices.Contains(res.Published, key); got != row.published { t.Fatalf("published %v, not published %v", res.Published, res.NotPublished) } + // The state holds the bare membership the assignments stream carries, through mesh-sdk's codec, and + // only one published (issue 774); what became of it is the kept pass's, which grants_membership reads. + state, _, inState, _ := b.ownGet(grants.StateMemberships, key) + ms, _ := partOn(b).MembershipOf("a", "mod-x") + pass := fmt.Sprint(ms.(map[string]any)["last_pass"]) if row.published { if row.names == "" && row.last != nil && !strings.Contains(string(b.stream["mesh.assignment."+key]), "10.0.0.1") { t.Errorf("the private network of the last membership is not carried: %s", b.stream["mesh.assignment."+key]) } - if raw, _, _, _ := b.ownGet(grants.StateMemberships, key); !strings.Contains(string(raw), row.names) { - t.Errorf("the pass does not name %q: %s", row.names, raw) + got, err := membership.DecodeGrantsMembership(state) + var sent membership.Membership + _ = json.Unmarshal(b.stream["mesh.assignment."+key], &sent) + if err != nil || !reflect.DeepEqual(got, sent) || bytes.Contains(state, []byte(`"membership"`)) { + t.Errorf("the state is not the bare membership published (%v): %s", err, state) + } + if !strings.Contains(pass, row.names) { + t.Errorf("the pass does not name %q: %s", row.names, pass) } return } + if inState { + t.Errorf("a membership held back reached the state: %s", state) + } said := strings.Join(res.NotPublished, "\n") if !strings.Contains(said, grants.ShadowDifference) || !strings.Contains(said, row.names) { t.Errorf("the loss is not named: %s", said) } - ms, _ := partOn(b).MembershipOf("a", "mod-x") - if !strings.Contains(fmt.Sprint(ms), row.names) { + if !strings.Contains(pass, row.names) { t.Errorf("grants_membership does not name what would be lost: %v", ms) } }) diff --git a/modules/mesh-grants/internal/grants/part.go b/modules/mesh-grants/internal/grants/part.go index 38da1f4..1e83f5c 100644 --- a/modules/mesh-grants/internal/grants/part.go +++ b/modules/mesh-grants/internal/grants/part.go @@ -106,8 +106,12 @@ type Result struct { MembershipsWritten int `json:"memberships_written"` Published []string `json:"published,omitempty"` NotPublished []string `json:"not_published,omitempty"` - Assertions int `json:"assertions"` - AssertionsWritten int `json:"assertions_written"` + // Retired is, by `.`, 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). + Retired map[string][]string `json:"retired,omitempty"` + Assertions int `json:"assertions"` + AssertionsWritten int `json:"assertions_written"` // Asserted is each module whose declared streams this pass created, ` at : created …`, and // NotAsserted each creation refused, ` : ` (issue 1790). Asserted []string `json:"asserted,omitempty"` @@ -320,9 +324,19 @@ func (p *Part) Compose() Result { case m.Published != MembershipUnchanged: res.NotPublished = append(res.NotPublished, key+": "+m.Published) } - data, _ := json.Marshal(m) - if p.changed(&res, StateMemberships, key, data) { - res.MembershipsWritten++ + if len(m.Retired) > 0 { + if res.Retired == nil { + res.Retired = map[string][]string{} + } + res.Retired[key] = m.Retired + } + // The state holds the bare membership as the assignments stream carries it, through mesh-sdk's one codec (issue + // 774): only one published now or unchanged, so a membership held back never reaches a reader by this way. What + // became of each at this pass is the pass's own (Published, NotPublished, Retired), kept with it (Pass). + if now || m.Published == MembershipUnchanged { + if p.changed(&res, StateMemberships, key, membership.EncodeGrantsMembership(m.Membership)) { + res.MembershipsWritten++ + } } } held, heldErr := HeldStreams(p.Call) diff --git a/modules/mesh-grants/internal/grants/tools.go b/modules/mesh-grants/internal/grants/tools.go index defb2f5..3c0bb57 100644 --- a/modules/mesh-grants/internal/grants/tools.go +++ b/modules/mesh-grants/internal/grants/tools.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/membership" ) // The part's tools, as its manifest lists them. Module tools in this milestone: a `mesh-*` seat must be in mesh-sdk's own @@ -133,16 +135,52 @@ func (p *Part) MembershipOf(node, module string) (any, error) { } out["composed"] = why } - if raw, rev, found, err := p.Store.Get(StateMemberships, node+"."+module); err != nil { + // The state holds the bare membership last published (mesh-sdk's codec, issue 774); what became of it at the last + // pass is the pass's, kept in user-list at KeyPass. + key := node + "." + module + if raw, rev, found, err := p.Store.Get(StateMemberships, key); err != nil { out["published"] = "not readable: " + err.Error() - } else if found { - out["published"] = map[string]any{"revision": rev, "last_pass": json.RawMessage(raw)} + } else if !found { + out["published"] = "no pass of the part's process has published it yet" + } else if was, err := membership.DecodeGrantsMembership(raw); err != nil { + out["published"] = fmt.Sprintf("revision %d is not readable: %v", rev, err) } else { - out["published"] = "no pass of the part's process has composed it yet" + out["published"] = map[string]any{"revision": rev, "digest": digestOf(was), "membership": was} } + out["last_pass"] = p.passOf(key) return out, nil } +// passOf is what the last pass the part's process kept says of one membership: published, unchanged or why not, and +// the seat verbs it dropped as retired. +func (p *Part) passOf(key string) any { + raw, rev, found, err := p.Store.Get(StateUserList, KeyPass) + if err != nil { + return "not readable: " + err.Error() + } + if !found { + return "no pass of the part's process has been kept yet" + } + var res Result + if err := json.Unmarshal(raw, &res); err != nil { + return "not readable: " + err.Error() + } + said := MembershipUnchanged + if slices.Contains(res.Published, key) { + said = MembershipPublished + } + for _, n := range res.NotPublished { + if k, why, _ := strings.Cut(n, ": "); k == key { + said = why + } + } + out := map[string]any{"revision": rev, "published": said} + if r := res.Retired[key]; len(r) > 0 { + out["retired"] = r + } + return out +} + // Held answers every membership the part's process held back at its last pass, each `.` with why (what // publishing would lose), from the pass it keeps (issue 1945). func (p *Part) Held() (any, error) {