Merge pull request 'mesh-grants writes its memberships bare through the mesh-sdk codec (issue 774)' (#114) from fix/memberships-shape into main

Reviewed-on: #114
This commit was merged in pull request #114.
This commit is contained in:
2026-10-30 14:42:13 +00:00
5 changed files with 87 additions and 19 deletions
+1 -1
View File
@@ -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
)
+2 -2
View File
@@ -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=
+23 -7
View File
@@ -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)
}
})
+19 -5
View File
@@ -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 `<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).
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, `<module> at <digest>: created …`, and
// NotAsserted each creation refused, `<module> <stream>: <why>` (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)
+42 -4
View File
@@ -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 `<node>.<module>` with why (what
// publishing would lose), from the pass it keeps (issue 1945).
func (p *Part) Held() (any, error) {