Stop hearing mesh.assignment: a process follows its membership from the grants part's state alone
ADR 0571 clause 10: the grants part publishes memberships on mesh.assignment until the lease write and stops then. FollowMembership now reads and watches its own key of the state memberships and nothing else, and refuses to follow, naming the state, when it cannot read it (there is nothing else to follow, and a silent follow of nothing is the fault). A module's principal and the runtime lose their subscribe on mesh.assignment; the grants part's publish grant stays until the write (S21 stops the publishing). Tests: TestFollowMembershipHearsWhatIsIssuedToTheAccount (integration) edited: no state is a refusal, a membership put in the state is applied, one published on the subject is not; run locally against its embedded bus. Rows added to TestTheCopiedComposerComposesAsTheControllers: nobody subscribes mesh.assignment, and the controller's golden grant of it is the one row the copy now lacks. No new test function. (issue 774, 774/S5) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -161,8 +161,11 @@ func grantsAsTheControllers(t *testing.T, got, want string) {
|
|||||||
intended := beyondTheControllers(user)
|
intended := beyondTheControllers(user)
|
||||||
for g := range was {
|
for g := range was {
|
||||||
// A runtime's direct get of its membership from the assignments stream went with the stream (research 072
|
// A runtime's direct get of its membership from the assignments stream went with the stream (research 072
|
||||||
// row 33): it reads the grants part's state memberships instead (ADR 0571 clause 7).
|
// row 33): it reads the grants part's state memberships instead (ADR 0571 clause 7). And it no longer hears
|
||||||
if !now[g] && !strings.HasPrefix(g, "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment."+strings.SplitN(user, ".", 2)[0]+".") {
|
// the subject, where the grants part stops publishing at the lease write (ADR 0571 clause 10, issue 774).
|
||||||
|
node := strings.SplitN(user, ".", 2)[0]
|
||||||
|
if !now[g] && !strings.HasPrefix(g, "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment."+node+".") &&
|
||||||
|
!strings.HasPrefix(g, "mesh.assignment."+node+".") {
|
||||||
t.Errorf("%s: the copy does not grant %s, which the controller's does", user, g)
|
t.Errorf("%s: the copy does not grant %s, which the controller's does", user, g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -880,6 +883,17 @@ func healthChecks(t *testing.T) {
|
|||||||
if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") {
|
if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") {
|
||||||
t.Errorf("%s writes the assigner's policies", u.Username())
|
t.Errorf("%s writes the assigner's policies", u.Username())
|
||||||
}
|
}
|
||||||
|
// ADR 0571 clause 10 (issue 774, 774/S5/AC2): nobody hears memberships on mesh.assignment; they are read from
|
||||||
|
// the grants part's state.
|
||||||
|
if MaySubscribe(perms, "mesh.assignment.a.restic") {
|
||||||
|
t.Errorf("%s hears mesh.assignment", u.Username())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nor the runtime carrying the node's modules, which reads each one's membership from the state.
|
||||||
|
if perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "a", Module: RuntimeModule,
|
||||||
|
Carries: []Declared{{Module: "restic"}}}); err != nil || MaySubscribe(perms, "mesh.assignment.a.restic") ||
|
||||||
|
!MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.a.restic") {
|
||||||
|
t.Errorf("the runtime hears mesh.assignment or reads no membership: %v", err)
|
||||||
}
|
}
|
||||||
// 1953 S-age (ADR 0565 §10): a log that says an age keeps its entries that long and no longer; one that says none, as
|
// 1953 S-age (ADR 0565 §10): a log that says an age keeps its entries that long and no longer; one that says none, as
|
||||||
// long as the log (ADR 0297).
|
// long as the log (ADR 0297).
|
||||||
|
|||||||
@@ -1075,10 +1075,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sub = append(sub, announcing(announced...)...)
|
sub = append(sub, announcing(announced...)...)
|
||||||
// Its own membership (ADR 0160): the one subject a runtime derives for itself, heard live. Nothing else's.
|
// Its own membership (ADR 0160), from the grants part's state memberships, its own key (novox/hq ADR 0571 clause
|
||||||
sub = append(sub, MembershipSubject(p.Node, p.Module))
|
// 7): its one read of it, since the assignments stream's direct get went with the stream (research 072 row 33),
|
||||||
// And from the grants part's state memberships, its own key (novox/hq ADR 0571 clause 7): its one read of it,
|
// and no longer heard on its subject, where the grants part stops publishing at the lease write (ADR 0571 clause
|
||||||
// since the assignments stream's direct get went with the stream (research 072 row 33).
|
// 10, issue 774).
|
||||||
pub = append(pub, readGrants(GrantsMembershipsBucket, MembershipKey(p.Node, p.Module))...)
|
pub = append(pub, readGrants(GrantsMembershipsBucket, MembershipKey(p.Node, p.Module))...)
|
||||||
|
|
||||||
// 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same
|
// 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same
|
||||||
@@ -1240,10 +1240,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Every assigned module's membership on this node (ADR 0160): one per module, heard live and read from the
|
// Every assigned module's membership on this node (ADR 0160): one per module, read and watched from the
|
||||||
// grants part's state memberships (ADR 0571 clause 7). This node's and no other's — the one token
|
// grants part's state memberships (ADR 0571 clause 7), and no longer heard on `mesh.assignment`, where the grants
|
||||||
// that varies is the module, so the pattern is the machine's own assignments.
|
// part stops publishing at the lease write (ADR 0571 clause 10, issue 774). This node's and no other's — the
|
||||||
sub = append(sub, "mesh.assignment."+p.Node+".*")
|
// one token that varies is the module, so the pattern is the machine's own assignments.
|
||||||
pub = append(pub, readGrants(GrantsMembershipsBucket, p.Node+".*")...)
|
pub = append(pub, readGrants(GrantsMembershipsBucket, p.Node+".*")...)
|
||||||
// And that it is there (novox/hq to-be 45 §3, S11): its own heartbeat, under its machine's
|
// And that it is there (novox/hq to-be 45 §3, S11): its own heartbeat, under its machine's
|
||||||
// name and no other's, on core NATS like the host's.
|
// name and no other's, on core NATS like the host's.
|
||||||
|
|||||||
+7
-15
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||||
@@ -17,10 +16,10 @@ import (
|
|||||||
// not a membership is handed to onBad (nil to ignore it) and skipped.
|
// not a membership is handed to onBad (nil to ignore it) and skipped.
|
||||||
//
|
//
|
||||||
// It reads the membership from the grants part's state memberships, its own key (novox/hq ADR 0571 clause 7),
|
// It reads the membership from the grants part's state memberships, its own key (novox/hq ADR 0571 clause 7),
|
||||||
// watched so the value there now comes first and each change after it; and it hears the subject the grants part
|
// watched so the value there now comes first and each change after it. Nothing else: the subject the grants part
|
||||||
// publishes on until the lease write. The assignments stream is no longer read: the bus module's tools delete it
|
// published on is no longer heard (ADR 0571 clause 10, issue 774), since it stops publishing at the lease write, and
|
||||||
// at the lease write (research 072 row 33). A state not there yet, or not readable, is told to onBad and the
|
// the assignments stream is no longer read (research 072 row 33). A state not there or not readable is a refusal to
|
||||||
// subject is followed alone. The returned error is a refusal to follow at all.
|
// follow, returned: there is nothing else to follow.
|
||||||
func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(error)) error {
|
func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(error)) error {
|
||||||
if c.account.Node == "" || c.account.Module == "" {
|
if c.account.Node == "" || c.account.Module == "" {
|
||||||
return fmt.Errorf("the account names no node or module, so it has no membership")
|
return fmt.Errorf("the account names no node or module, so it has no membership")
|
||||||
@@ -38,27 +37,20 @@ func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(er
|
|||||||
}
|
}
|
||||||
apply(issued)
|
apply(issued)
|
||||||
}
|
}
|
||||||
subject := membership.Subject(c.account.Node, c.account.Module)
|
|
||||||
if _, err := c.nc.Subscribe(subject, func(m *nats.Msg) { take(m.Data) }); err != nil {
|
|
||||||
return fmt.Errorf("cannot follow %s: %w", subject, err)
|
|
||||||
}
|
|
||||||
key := membership.GrantsKey(c.account.Node, c.account.Module)
|
key := membership.GrantsKey(c.account.Node, c.account.Module)
|
||||||
js, err := jetstream.New(c.nc)
|
js, err := jetstream.New(c.nc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bad(fmt.Errorf("cannot read %s of %s (%v); following %s alone", key, membership.GrantsBucket, err, subject))
|
return fmt.Errorf("cannot read %s of %s: %w", key, membership.GrantsBucket, err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
kv, err := js.KeyValue(ctx, membership.GrantsBucket)
|
kv, err := js.KeyValue(ctx, membership.GrantsBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bad(fmt.Errorf("cannot read %s of %s yet (%v); following %s alone", key, membership.GrantsBucket, err, subject))
|
return fmt.Errorf("cannot read %s of %s: %w", key, membership.GrantsBucket, err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
w, err := kv.Watch(context.Background(), key)
|
w, err := kv.Watch(context.Background(), key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bad(fmt.Errorf("cannot watch %s of %s (%v); following %s alone", key, membership.GrantsBucket, err, subject))
|
return fmt.Errorf("cannot watch %s of %s: %w", key, membership.GrantsBucket, err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
for e := range w.Updates() {
|
for e := range w.Updates() {
|
||||||
|
|||||||
@@ -3,14 +3,20 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A process connected pinned to a test bus hears the membership published for its node and module, and
|
// A process connected pinned to a test bus follows the membership the grants part writes for its node and module in
|
||||||
// ignores what is not one (novox/hq issue 1633, AC2).
|
// its state memberships, and reports what is not one (novox/hq issue 1633, AC2); it no longer hears mesh.assignment,
|
||||||
|
// where the grants part stops publishing at the lease write (ADR 0571 clause 10; issue 774, 774/S5/AC1). Without the
|
||||||
|
// state it refuses to follow, naming it.
|
||||||
func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) {
|
func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) {
|
||||||
url, fingerprint := selfSignedServer(t)
|
url, fingerprint := selfSignedServer(t)
|
||||||
account := mailSenderAccount(t, url, fingerprint)
|
account := mailSenderAccount(t, url, fingerprint)
|
||||||
@@ -20,26 +26,52 @@ func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
got := make(chan membership.Membership, 1)
|
got := make(chan membership.Membership, 4)
|
||||||
bad := make(chan error, 4)
|
bad := make(chan error, 4)
|
||||||
if err := conn.FollowMembership(func(m membership.Membership) { got <- m }, func(e error) { bad <- e }); err != nil {
|
follow := func() error {
|
||||||
|
return conn.FollowMembership(func(m membership.Membership) { got <- m }, func(e error) { bad <- e })
|
||||||
|
}
|
||||||
|
if err := follow(); err == nil || !strings.Contains(err.Error(), membership.GrantsBucket) {
|
||||||
|
t.Fatalf("followed with no state memberships: %v", err)
|
||||||
|
}
|
||||||
|
js, err := jetstream.New(conn.nc)
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte("not json")); err != nil {
|
ctx := context.Background()
|
||||||
|
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: membership.GrantsBucket})
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte(`{"node":"novox","module":"mesh-mail","mesh":["10.0.0.1"]}`)); err != nil {
|
key := membership.GrantsKey("novox", "mesh-mail")
|
||||||
|
if _, err := kv.Put(ctx, key, []byte("not json")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := follow(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Published on the subject, as the grants part did until the write: not heard.
|
||||||
|
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte(`{"node":"novox","module":"mesh-mail","mesh":["10.0.0.9"]}`)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := kv.Put(ctx, key, membership.EncodeGrantsMembership(membership.Membership{Node: "novox", Module: "mesh-mail",
|
||||||
|
Mesh: []string{"10.0.0.1"}})); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case m := <-got:
|
case m := <-got:
|
||||||
if m.Module != "mesh-mail" || len(m.Mesh) != 1 {
|
if m.Module != "mesh-mail" || len(m.Mesh) != 1 || m.Mesh[0] != "10.0.0.1" {
|
||||||
t.Fatalf("membership: %+v", m)
|
t.Fatalf("membership: %+v", m)
|
||||||
}
|
}
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
t.Fatal("no membership heard")
|
t.Fatal("no membership followed")
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case m := <-got:
|
||||||
|
t.Errorf("a second membership was applied, from the subject: %+v", m)
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
}
|
}
|
||||||
if len(bad) == 0 {
|
if len(bad) == 0 {
|
||||||
t.Error("the body that is not a membership was not reported")
|
t.Error("the value that is not a membership was not reported")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user