Merge pull request 'mesh-grants: create a declared module's missing buckets, logs and streams on the event; publish memberships past retired seat verbs (issues 1790, 1945)' (#58) from fix/1790-grants-create-buckets into main
This commit was merged in pull request #58.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"mesh-grants/internal/grants"
|
||||
)
|
||||
|
||||
// creating creates a stream a manifest declares that the bus lacks (issue 1790; the operator, 2026-10-26 08:54): the
|
||||
// create alone, which the account is granted by the stream's name, never an update or a delete. The one place the part
|
||||
// creates on the bus.
|
||||
type creating struct {
|
||||
ctx context.Context
|
||||
js jetstream.JetStream
|
||||
}
|
||||
|
||||
func (c creating) CreateDeclared(cfg jetstream.StreamConfig) error {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
||||
defer cancel()
|
||||
_, err := c.js.CreateStream(ctx, cfg)
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, jetstream.ErrStreamNameAlreadyInUse):
|
||||
return grants.ErrHeld
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return fmt.Errorf("no answer in %s: a create the bus refuses is answered by none (%w)", requestWithin, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -5,7 +5,10 @@
|
||||
//
|
||||
// **Memberships are published for real** (the operator, 2026-10-25 22:31): since release 6 nobody publishes them, so a
|
||||
// module placed since cannot serve its seat or reach a new state. Each is published on the assignments stream only when
|
||||
// it differs from the one last published there. **The user list, the assertions and the grants stay in shadow** until
|
||||
// it differs from the one last published there. **A bucket, log or stream a manifest declares that the bus lacks is
|
||||
// created** (the operator, 2026-10-26 08:54; issue 1790), once per manifest, with the manifest's limits, on the event
|
||||
// that brought the manifest; one the bus holds is never changed. **The user list, the rest of the assertions and the
|
||||
// grants stay in shadow** until
|
||||
// the operator's take-over, which this milestone always refuses: they are composed, compared with what the bus loaded
|
||||
// and holds (through the bus module's own read tools) and kept in the part's own state, and take no effect.
|
||||
//
|
||||
@@ -112,7 +115,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
bus := newBusReader(ctx, js)
|
||||
f.Part = &grants.Part{Read: bus, Call: conn, Store: ownStore{conn}, Emit: emitting{conn},
|
||||
Publish: memberships{ctx: ctx, nc: nc, js: js}}
|
||||
Publish: memberships{ctx: ctx, nc: nc, js: js}, Create: creating{ctx: ctx, js: js}}
|
||||
f.Watch = bus
|
||||
f.Asked = func(wake func()) error {
|
||||
return conn.State(grants.StateUserList).Watch(grants.KeyAsked, func(c stdio.StateChange) error {
|
||||
@@ -122,7 +125,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return nil
|
||||
})
|
||||
}
|
||||
logger.Printf("started on the account %s: publishing memberships; the user list, assertions and grants in shadow", account.User)
|
||||
logger.Printf("started on the account %s: publishing memberships and creating the declared buckets, logs and streams the bus lacks; the user list, grants and any change to a held stream in shadow", account.User)
|
||||
f.Run(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,9 +67,10 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) { return p.Grants(grants.Answer(a, "module")) }},
|
||||
{Name: grants.ToolMembership, Description: "One module's membership on one node (ADR 0547 clause 6): as composed now from the " +
|
||||
"bus's records with the bus composer, and what the part's process published of it at its last pass (published, " +
|
||||
"unchanged, or why not: a shadow difference names what publishing would lose), with what the composition cannot " +
|
||||
"read. Read-only.",
|
||||
Input: schema(map[string]string{"node": "the machine", "module": "the module"}, "node", "module"),
|
||||
"unchanged, or why not: a shadow difference names what publishing would lose; verbs the seat catalogue retired are " +
|
||||
"dropped and named), with what the composition cannot read. With neither node nor module: every membership the last " +
|
||||
"pass held back, each with what it would lose. Read-only.",
|
||||
Input: schema(map[string]string{"node": "the machine (with module)", "module": "the module (with node)"}),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.MembershipOf(grants.Answer(a, "node"), grants.Answer(a, "module"))
|
||||
}},
|
||||
@@ -90,8 +91,10 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
"verb, and the answer says so.",
|
||||
Run: func(a map[string]any) (any, error) { return p.AskCompose(stdio.CallerOf(a)) }},
|
||||
{Name: grants.ToolAssert, Description: "One module's buckets, logs and streams (ADR 0547 clause 6), composed from its manifest at " +
|
||||
"the indexer's trunk with their limits, beside what the bus holds (through the bus module's nats_streams). In shadow it " +
|
||||
"asserts nothing, and says so.",
|
||||
"the indexer's trunk with their limits, beside what the bus holds (through the bus module's nats_streams). When the " +
|
||||
"bus lacks one, asks the part's process (which alone holds the grant) to create it now with the manifest's limits, " +
|
||||
"as it does by itself when a manifest is registered (issue 1790), and says asserted; one the bus holds is never " +
|
||||
"changed, a difference is only said. Answers the comparison and the ask; call again to see what the bus holds.",
|
||||
Input: schema(map[string]string{"module": "the module"}, "module"),
|
||||
Run: func(a map[string]any) (any, error) { return p.AssertOne(grants.Answer(a, "module")) }},
|
||||
{Name: grants.ToolTakeOver, Description: "The operator's switch to the grants part's list (ADR 0547 clause 8), refused without the " +
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-grants
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025215021-b7b5c6ef27a0
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026080443-136fa8b2a896
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025215021-b7b5c6ef27a0 h1:8dKGrZzPthx/jZlV9Bjm2lWGNORUTETmq2vxKN+MRV0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025215021-b7b5c6ef27a0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026080443-136fa8b2a896 h1:5rHfru5NJ7SK20QOt8YUJSglcn1gsjtLc9AGmWYGnRQ=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026080443-136fa8b2a896/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=
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
@@ -43,6 +45,9 @@ type fakeBus struct {
|
||||
refusePub bool
|
||||
// hold, when set, keeps every nats_users call waiting until it is closed: a pass waiting on the bus.
|
||||
hold chan struct{}
|
||||
// creates are the streams the part asked to create, refused when refuseCreate.
|
||||
creates []string
|
||||
refuseCreate bool
|
||||
}
|
||||
|
||||
func (b *fakeBus) LastMembership(node, module string) ([]byte, uint64, bool, error) {
|
||||
@@ -72,6 +77,25 @@ func (b *fakeBus) PublishMembership(node, module string, body []byte, after uint
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDeclared is the bus's create of a stream: refused when refuseCreate, held when the bus holds it, and otherwise
|
||||
// held from now on, as nats_streams then reads it.
|
||||
func (b *fakeBus) CreateDeclared(cfg jetstream.StreamConfig) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.creates = append(b.creates, cfg.Name)
|
||||
if b.refuseCreate {
|
||||
return errors.New("Permissions Violation for Publish to $JS.API.STREAM.CREATE." + cfg.Name)
|
||||
}
|
||||
for _, s := range b.streams {
|
||||
if s["name"] == cfg.Name {
|
||||
return grants.ErrHeld
|
||||
}
|
||||
}
|
||||
b.streams = append(b.streams, map[string]any{"name": cfg.Name, "subjects": cfg.Subjects, "retention": "limits",
|
||||
"max_age_hours": int(cfg.MaxAge.Hours()), "max_per_subject": int(cfg.MaxMsgsPerSubject)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFakeBus() *fakeBus {
|
||||
return &fakeBus{states: map[string]map[string][]byte{}, refused: map[string]bool{}, own: map[string]map[string][]byte{},
|
||||
revs: map[string]uint64{}, refuseTo: map[string]bool{}}
|
||||
@@ -295,12 +319,14 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no timer, no create, a publish only of memberships", func(t *testing.T) {
|
||||
t.Run("no timer, no update or delete, a publish only of memberships, a create only of declared streams", func(t *testing.T) {
|
||||
forbidden := []string{"time.Tick", "time.NewTicker", "time.Sleep", "time.After", "time.NewTimer", "time.AfterFunc",
|
||||
"PublishMsg", "CreateStream", "CreateKeyValue", "CreateOrUpdate", "UpdateStream", "UpdateKeyValue",
|
||||
"CreateObjectStore", "EnsureStream", "RaiseBuckets", "RaiseLogs", ".Create("}
|
||||
// The one publish: a membership, on its own subject, in publish.go.
|
||||
"PublishMsg", "CreateKeyValue", "CreateOrUpdate", "UpdateStream", "UpdateKeyValue", "DeleteStream", "PurgeStream",
|
||||
"DeleteKeyValue", "CreateObjectStore", "EnsureStream", "RaiseBuckets", "RaiseLogs", ".Create("}
|
||||
// The one publish: a membership, on its own subject, in publish.go; the one create: a declared stream the bus lacks,
|
||||
// in create.go (issue 1790).
|
||||
publishOnly := filepath.Join("cmd", "mesh-grants", "publish.go")
|
||||
createOnly := filepath.Join("cmd", "mesh-grants", "create.go")
|
||||
files := 0
|
||||
for _, root := range []string{"cmd", "internal"} {
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
@@ -315,6 +341,9 @@ func TestGrants(t *testing.T) {
|
||||
if path == publishOnly && strings.Count(string(src), ".Publish(") != 1 {
|
||||
t.Errorf("publish.go publishes other than one membership")
|
||||
}
|
||||
if n := strings.Count(string(src), "CreateStream"); (path == createOnly && n != 1) || (path != createOnly && n != 0) {
|
||||
t.Errorf("%s holds CreateStream %d times: only create.go creates, once", path, n)
|
||||
}
|
||||
for _, f := range forbidden {
|
||||
if strings.Contains(string(src), f) {
|
||||
t.Errorf("%s holds %s: the part holds no timer and creates or asserts nothing", path, f)
|
||||
@@ -513,8 +542,9 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
|
||||
a, err := p.AssertOne("mod-x")
|
||||
as, _ := a.(grants.Assertion)
|
||||
if err != nil || as.Asserted || as.Says != grants.ShadowAsserts || len(as.Items) != 3 {
|
||||
am, _ := a.(map[string]any)
|
||||
as, _ := am["assertion"].(grants.Assertion)
|
||||
if err != nil || as.Asserted || as.Says != grants.ShadowAsserts || len(as.Items) != 3 || am["asked"] == nil {
|
||||
t.Fatalf("assert = %+v, %v", a, err)
|
||||
}
|
||||
for _, it := range as.Items {
|
||||
@@ -639,6 +669,17 @@ func TestGrants(t *testing.T) {
|
||||
m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.mod-x.tool.gone"})
|
||||
return m
|
||||
}, false, "tool.gone"},
|
||||
// A verb the seat catalogue retired on purpose (mark-ready, mesh-sdk d506f21, ADR 0421) is dropped and named; a
|
||||
// verb of a seat the catalogue does not define is still a loss (issue 1945).
|
||||
{"a change that drops only a retired seat verb is published, the verb named", func(m map[string]any) map[string]any {
|
||||
m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"}}
|
||||
return m
|
||||
}, true, "mesh-delivery mark-ready"},
|
||||
{"a retired verb and a module seat's verb dropped: not published, the module seat's named", func(m map[string]any) map[string]any {
|
||||
m["seats"] = []any{map[string]any{"seat": "mesh-delivery", "verb": "mark-ready", "subject": "mesh.seat.mesh-delivery.tool.mark-ready"},
|
||||
map[string]any{"seat": "telegram-sender", "verb": "send", "subject": "mesh.seat.telegram-sender.tool.send"}}
|
||||
return m
|
||||
}, false, "telegram-sender"},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
b := fixture()
|
||||
@@ -653,9 +694,12 @@ func TestGrants(t *testing.T) {
|
||||
t.Fatalf("published %v, not published %v", res.Published, res.NotPublished)
|
||||
}
|
||||
if row.published {
|
||||
if row.last != nil && !strings.Contains(string(b.stream["mesh.assignment."+key]), "10.0.0.1") {
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
said := strings.Join(res.NotPublished, "\n")
|
||||
@@ -707,6 +751,138 @@ func TestGrants(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("assertions: what the bus lacks is created once, what it holds is never touched", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
// The bus holds mod-x's bucket with another history, and neither its log nor its stream.
|
||||
b.streams = []map[string]any{{"name": "KV_mod-x_kept", "subjects": []string{"$KV.mod-x_kept.>"}, "retention": "limits", "max_per_subject": 2}}
|
||||
p := partOn(b)
|
||||
p.Create = b
|
||||
assertion := func() grants.Assertion {
|
||||
raw, _, _, _ := b.ownGet(grants.StateAssertions, "mod-x")
|
||||
var a grants.Assertion
|
||||
_ = json.Unmarshal(raw, &a)
|
||||
return a
|
||||
}
|
||||
first := p.Compose()
|
||||
if !slices.Equal(b.creates, []string{"LOG_mod-x_history", "THINGS"}) || len(first.Asserted) != 1 ||
|
||||
!strings.HasPrefix(first.Asserted[0], "mod-x at ") || !slices.Contains(b.events, grants.EventAsserted) {
|
||||
t.Fatalf("the first pass created %v, said %v, events %v", b.creates, first.Asserted, b.events)
|
||||
}
|
||||
a := assertion()
|
||||
if !a.Asserted || a.Says != grants.CreatesMissing {
|
||||
t.Errorf("the assertion kept = %+v", a)
|
||||
}
|
||||
for _, it := range a.Items {
|
||||
switch it.Stream {
|
||||
case "KV_mod-x_kept":
|
||||
if it.Created != "" || !slices.Contains(it.Differences, "history 2 held, 3 composed") {
|
||||
t.Errorf("a bucket the bus holds was touched, or its difference not said: %+v", it)
|
||||
}
|
||||
default:
|
||||
if it.Created != grants.ItemCreated {
|
||||
t.Errorf("%s: %q, want created", it.Stream, it.Created)
|
||||
}
|
||||
}
|
||||
}
|
||||
if again := p.Compose(); len(again.Asserted) != 0 || len(b.creates) != 2 {
|
||||
t.Errorf("a pass with everything held created again: %v, %v", again.Asserted, b.creates)
|
||||
}
|
||||
// THINGS deleted since: not made again at the same manifest, said; a hand assert (the tool, asking the process)
|
||||
// makes it again.
|
||||
b.mu.Lock()
|
||||
b.streams = slices.DeleteFunc(b.streams, func(s map[string]any) bool { return s["name"] == "THINGS" })
|
||||
b.mu.Unlock()
|
||||
gone := p.Compose()
|
||||
if len(b.creates) != 2 || !strings.Contains(strings.Join(gone.NotAsserted, " "), "THINGS: created at this manifest and gone since") {
|
||||
t.Errorf("a stream gone since was made again by itself, or not said: %v, %v", b.creates, gone.NotAsserted)
|
||||
}
|
||||
tool := partOn(b)
|
||||
if got, err := tool.AssertOne("mod-x"); err != nil || fmt.Sprint(got) == "" || len(b.creates) != 2 {
|
||||
t.Fatalf("the tool = %v, %v, creates %v: it asks, and creates nothing itself", got, err, b.creates)
|
||||
}
|
||||
if hand := p.Compose(); len(hand.Asserted) != 1 || b.creates[len(b.creates)-1] != "THINGS" {
|
||||
t.Errorf("the hand assert did not create THINGS again: %v, %v", hand.Asserted, b.creates)
|
||||
}
|
||||
if after := p.Compose(); len(after.Asserted) != 0 {
|
||||
t.Errorf("the hand ask was acted on twice: %v", after.Asserted)
|
||||
}
|
||||
for _, c := range b.creates {
|
||||
if c == "KV_mod-x_kept" {
|
||||
t.Error("the bucket the bus holds was asked to be created")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assertions: a refused create is said, never stops it, and is tried at the next event", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
b.refuseCreate = true
|
||||
p := partOn(b)
|
||||
p.Create = b
|
||||
refused := p.Compose()
|
||||
if len(refused.Asserted) != 0 || !strings.Contains(strings.Join(refused.NotAsserted, " "), "Permissions Violation") ||
|
||||
slices.Contains(b.events, grants.EventAsserted) {
|
||||
t.Fatalf("a refused create = %+v", refused)
|
||||
}
|
||||
b.refuseCreate = false
|
||||
if next := p.Compose(); len(next.Asserted) != 1 || !strings.Contains(next.Asserted[0], "KV_mod-x_kept") {
|
||||
t.Errorf("the next event created %v", next.Asserted)
|
||||
}
|
||||
// What the bus holds unread: nothing is created.
|
||||
c := fixture()
|
||||
c.loaded = shadowOf(t, c)
|
||||
q := partOn(c)
|
||||
q.Create = c
|
||||
q.Call = refusingStreams{c}
|
||||
if r := q.Compose(); len(c.creates) != 0 || len(r.Asserted) != 0 {
|
||||
t.Errorf("created %v with what the bus holds unread", c.creates)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("memberships held back: every one is said and listed", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
|
||||
b.stream, b.seqs = map[string][]byte{}, map[string]uint64{}
|
||||
for i, key := range []string{"a.mod-x", "b.mod-y"} {
|
||||
raw, _ := json.Marshal(grants.Memberships(recs)[key].Membership)
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
m["serves"] = append(m["serves"].([]any), map[string]any{"subject": "mesh.mod.lost.tool.gone"})
|
||||
raw, _ = json.Marshal(m)
|
||||
subject := "mesh.assignment." + key
|
||||
b.stream[subject], b.seqs[subject] = raw, uint64(i+1)
|
||||
}
|
||||
var logged []string
|
||||
var lmu sync.Mutex
|
||||
w := &fakeWatcher{opened: make(chan string, 16)}
|
||||
f := &grants.Follower{Part: partOn(b), Watch: w, Logf: func(format string, args ...any) {
|
||||
lmu.Lock()
|
||||
logged = append(logged, fmt.Sprintf(format, args...))
|
||||
lmu.Unlock()
|
||||
}}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() { f.Run(ctx); close(done) }()
|
||||
waitFor(t, func() bool {
|
||||
lmu.Lock()
|
||||
defer lmu.Unlock()
|
||||
all := strings.Join(logged, "\n")
|
||||
return strings.Contains(all, "held a.mod-x: ") && strings.Contains(all, "held b.mod-y: ")
|
||||
})
|
||||
cancel()
|
||||
<-done
|
||||
held, err := partOn(b).MembershipOf("", "")
|
||||
h, _ := held.(map[string]any)
|
||||
if err != nil || h["count"] != 2 || !strings.Contains(fmt.Sprint(h["held"]), "tool.gone") {
|
||||
t.Errorf("grants_membership with neither node nor module = %v, %v", held, err)
|
||||
}
|
||||
if _, err := partOn(b).MembershipOf("a", ""); err == nil {
|
||||
t.Error("a node without a module was answered")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a refused read or write never stops it", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
@@ -756,6 +932,16 @@ func nonNil(s []string) []string {
|
||||
}
|
||||
|
||||
// fakeWatcher refuses a state's first watches as told, and hands changes to the watches it opened.
|
||||
// refusingStreams is the bus with nats_streams refused: what the bus holds cannot be read.
|
||||
type refusingStreams struct{ *fakeBus }
|
||||
|
||||
func (r refusingStreams) Call(key string, body any) (json.RawMessage, error) {
|
||||
if key == grants.CallStreams {
|
||||
return nil, errors.New("refused")
|
||||
}
|
||||
return r.fakeBus.Call(key, body)
|
||||
}
|
||||
|
||||
type fakeWatcher struct {
|
||||
mu sync.Mutex
|
||||
refuse map[string]int
|
||||
|
||||
@@ -2,9 +2,14 @@ package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
@@ -19,6 +24,9 @@ type Item struct {
|
||||
Held *Limits `json:"held,omitempty"`
|
||||
// Differences says how they differ, in words; empty when they agree in what can be read from here.
|
||||
Differences []string `json:"differences,omitempty"`
|
||||
// Created says what became of creating it when the bus lacked it: "created", or why not (issue 1790). Empty when
|
||||
// the bus holds it or no creation was asked here.
|
||||
Created string `json:"created,omitempty"`
|
||||
}
|
||||
|
||||
// Limits are the facts of a stream an assertion sets and the bus module's nats_streams reads back. Its size cap is not
|
||||
@@ -31,8 +39,8 @@ type Limits struct {
|
||||
MaxBytes int64 `json:"max_bytes,omitempty"`
|
||||
}
|
||||
|
||||
// Assertion is one module's declared buckets, logs and streams, composed and compared. Asserted is always false in
|
||||
// shadow: nothing is created or changed.
|
||||
// Assertion is one module's declared buckets, logs and streams, composed and compared. Asserted says this pass created
|
||||
// one the bus lacked (the operator, 2026-10-26 08:54); one the bus holds is never changed.
|
||||
type Assertion struct {
|
||||
Module string `json:"module"`
|
||||
Manifest string `json:"manifest"`
|
||||
@@ -41,9 +49,119 @@ type Assertion struct {
|
||||
Says string `json:"says"`
|
||||
}
|
||||
|
||||
// ShadowAsserts is what every assertion says in this milestone.
|
||||
// ShadowAsserts is what an assertion says where nothing may be created: in the tools, which ask the part's process.
|
||||
const ShadowAsserts = "in shadow: composed and compared, nothing asserted (ADR 0547 clause 5 asserts from take-over)"
|
||||
|
||||
// CreatesMissing is what an assertion of the part's process says (the operator, 2026-10-26 08:54; issue 1790).
|
||||
const CreatesMissing = "a bucket, log or stream the manifest declares and the bus lacks is created with the manifest's limits, once per " +
|
||||
"manifest; one the bus holds is never changed or deleted, a difference is only said (ADR 0547 clause 5 asserts the rest from take-over)"
|
||||
|
||||
// Creator creates one stream the bus does not hold, with the configuration given, and never updates or deletes one: the
|
||||
// part's process alone, whose account is granted the create of each declared stream by name (the operator, 2026-10-26
|
||||
// 08:54). A stream the bus holds already answers ErrHeld.
|
||||
type Creator interface {
|
||||
CreateDeclared(cfg jetstream.StreamConfig) error
|
||||
}
|
||||
|
||||
// ErrHeld is a create refused because the bus holds the stream already (made meanwhile): left as it is.
|
||||
var ErrHeld = errors.New("the bus holds it already, so it is left as it is")
|
||||
|
||||
// Created words of an item.
|
||||
const (
|
||||
ItemCreated = "created"
|
||||
ItemNotCreated = "not created: "
|
||||
)
|
||||
|
||||
// Configs are the streams a manifest declares, as the controller asserts them at its start (mesh-sdk buscompose), by
|
||||
// stream name. Pure.
|
||||
func Configs(m catalogue.Manifest) map[string]jetstream.StreamConfig {
|
||||
out := map[string]jetstream.StreamConfig{}
|
||||
for _, b := range bucketsOf(m) {
|
||||
c := buscompose.BucketStreamConfig(b)
|
||||
out[c.Name] = c
|
||||
}
|
||||
for _, l := range logsOf(m) {
|
||||
c := l.Config()
|
||||
out[c.Name] = c
|
||||
}
|
||||
for _, s := range streamsOf(m) {
|
||||
c := buscompose.ModuleStreamConfig(s)
|
||||
out[c.Name] = c
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// creations remembers each stream created, by the manifest digest it was created at: created once per manifest, so a
|
||||
// stream deleted since is not made again until a new manifest or a hand assert (grants_assert).
|
||||
type creations struct {
|
||||
mu sync.Mutex
|
||||
made map[string]string
|
||||
}
|
||||
|
||||
func (c *creations) madeAt(stream string) string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.made[stream]
|
||||
}
|
||||
|
||||
func (c *creations) keep(stream, digest string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.made == nil {
|
||||
c.made = map[string]string{}
|
||||
}
|
||||
c.made[stream] = digest
|
||||
}
|
||||
|
||||
// createMissing creates each item of a the bus lacks, with the manifest's limits, and says what became of it. It never
|
||||
// touches an item the bus holds, and does nothing when what the bus holds was not read. force asks again a stream
|
||||
// created at this manifest and gone since (a hand assert). It answers the streams created.
|
||||
func createMissing(c Creator, done *creations, a *Assertion, m catalogue.Manifest, force bool) []string {
|
||||
cfgs := Configs(m)
|
||||
var made []string
|
||||
a.Says = CreatesMissing
|
||||
for i := range a.Items {
|
||||
it := &a.Items[i]
|
||||
if it.Held != nil || !slices.Contains(it.Differences, "the bus holds no stream "+it.Stream) {
|
||||
continue
|
||||
}
|
||||
if !force && done.madeAt(it.Stream) == a.Manifest {
|
||||
it.Created = ItemNotCreated + "created at this manifest and gone since; grants_assert creates it again"
|
||||
continue
|
||||
}
|
||||
cfg, ok := cfgs[it.Stream]
|
||||
if !ok {
|
||||
it.Created = ItemNotCreated + "no configuration composed for it"
|
||||
continue
|
||||
}
|
||||
switch err := c.CreateDeclared(cfg); {
|
||||
case err == nil:
|
||||
it.Created = ItemCreated
|
||||
done.keep(it.Stream, a.Manifest)
|
||||
made = append(made, it.Kind+" "+it.Stream)
|
||||
case errors.Is(err, ErrHeld):
|
||||
it.Created = ItemNotCreated + ErrHeld.Error()
|
||||
default:
|
||||
it.Created = ItemNotCreated + err.Error() + " (tried again at the next event)"
|
||||
}
|
||||
}
|
||||
if len(made) > 0 {
|
||||
a.Asserted = true
|
||||
}
|
||||
return made
|
||||
}
|
||||
|
||||
// notCreated is each item of a creation refused, with why, as `<stream>: <why>`.
|
||||
func notCreated(a Assertion) []string {
|
||||
var out []string
|
||||
for _, it := range a.Items {
|
||||
if why, ok := strings.CutPrefix(it.Created, ItemNotCreated); ok && it.Created != "" {
|
||||
out = append(out, it.Stream+": "+why)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Composed is a module's buckets, logs and streams as an assertion would ask for them. Pure.
|
||||
func Composed(m catalogue.Manifest) []Item {
|
||||
var out []Item
|
||||
|
||||
@@ -3,6 +3,7 @@ package grants
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -35,6 +36,9 @@ type Follower struct {
|
||||
watched map[string]bool
|
||||
refused map[string]string
|
||||
have map[string]string
|
||||
// held and notAsserted are what the last pass held back and could not create, said again only when they change.
|
||||
held string
|
||||
notAsserted string
|
||||
}
|
||||
|
||||
func (f *Follower) init() {
|
||||
@@ -142,6 +146,33 @@ func (f *Follower) open() {
|
||||
}
|
||||
}
|
||||
|
||||
// said logs every membership the pass held back, each with what it would lose (issue 1945), and every declared stream
|
||||
// the pass could not create, each with why (issue 1790), when the set changes from the last pass's; and every stream it
|
||||
// created, one line per module.
|
||||
func (f *Follower) said(res Result) {
|
||||
if held := strings.Join(res.NotPublished, "\n"); held != f.held {
|
||||
f.held = held
|
||||
if len(res.NotPublished) == 0 {
|
||||
f.logf("no membership held back")
|
||||
} else {
|
||||
f.logf("%d memberships not published, tried again at the next event (grants_membership with no node and module lists them):",
|
||||
len(res.NotPublished))
|
||||
for _, n := range res.NotPublished {
|
||||
f.logf("held %s", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, a := range res.Asserted {
|
||||
f.logf("asserted %s", a)
|
||||
}
|
||||
if not := strings.Join(res.NotAsserted, "\n"); not != f.notAsserted {
|
||||
f.notAsserted = not
|
||||
for _, n := range res.NotAsserted {
|
||||
f.logf("not asserted %s", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run composes at start and after every change until ctx ends. It returns only when ctx ends.
|
||||
func (f *Follower) Run(ctx context.Context) {
|
||||
f.init()
|
||||
@@ -159,9 +190,7 @@ func (f *Follower) Run(ctx context.Context) {
|
||||
if len(res.Published) > 0 {
|
||||
f.logf("published %d memberships: %v", len(res.Published), res.Published)
|
||||
}
|
||||
if len(res.NotPublished) > 0 {
|
||||
f.logf("%d memberships not published, tried again at the next event; the first: %s", len(res.NotPublished), res.NotPublished[0])
|
||||
}
|
||||
f.said(res)
|
||||
if res.Written {
|
||||
f.logf("the shadow list moved to revision %d: %d users, %d differing from the loaded list",
|
||||
res.Revision, res.UserList.Users, len(res.UserList.Compare.Differing()))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Package grants is the grants part in shadow (novox/hq ADR 0547 clauses 5 to 8, issue 1945): it composes the bus's user
|
||||
// list, each module's membership and each manifest's buckets, logs and streams from what the bus holds, with the bus
|
||||
// composer the controller uses (mesh-sdk buscompose), and compares them with what the bus loaded and holds. It writes only
|
||||
// its own state and takes no effect: it publishes no membership, asserts nothing, writes no grant and no user list the
|
||||
// bus node reads, until the operator's take-over, which this milestone always refuses.
|
||||
// composer the controller uses (mesh-sdk buscompose), and compares them with what the bus loaded and holds. Beyond its own
|
||||
// state it takes effect in two things alone, each by a grant the operator allowed: it publishes memberships (2026-10-25
|
||||
// 22:31), and creates a bucket, log or stream a manifest declares that the bus lacks (2026-10-26 08:54, issue 1790). It
|
||||
// changes no stream the bus holds, writes no grant and no user list the bus node reads, until the operator's take-over,
|
||||
// which this milestone always refuses.
|
||||
package grants
|
||||
|
||||
import (
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||
)
|
||||
|
||||
@@ -41,6 +42,9 @@ type Membership struct {
|
||||
// Published says what became of it at the last pass: "published", "unchanged" (equal to the one last published),
|
||||
// or why it was not.
|
||||
Published string `json:"published"`
|
||||
// Retired are the seat verbs the last membership served that mesh-sdk's seat catalogue no longer has, `<seat>
|
||||
// <verb>`: dropped on purpose, so their loss does not hold the membership back (issue 1945).
|
||||
Retired []string `json:"retired,omitempty"`
|
||||
}
|
||||
|
||||
// Memberships composes every assignment's membership from the records (buscompose.MembershipFor, as the controller's
|
||||
@@ -101,7 +105,9 @@ func publish(p Publisher, m Membership) (Membership, bool) {
|
||||
// composed with no root-free judgement, a kind's verified-sender would be dropped, and the operator's approvals by
|
||||
// a channel would no longer be honoured. Any loss stays a shadow difference, named.
|
||||
if found {
|
||||
if lost := Losses(was, body); len(lost) > 0 {
|
||||
kept, retired := WithoutRetired(was)
|
||||
m.Retired = retired
|
||||
if lost := Losses(kept, body); len(lost) > 0 {
|
||||
m.Published = ShadowDifference + strings.Join(lost, "; ")
|
||||
return m, false
|
||||
}
|
||||
@@ -114,6 +120,61 @@ func publish(p Publisher, m Membership) (Membership, bool) {
|
||||
return m, true
|
||||
}
|
||||
|
||||
// WithoutRetired is a membership without the seat verbs it serves that mesh-sdk's seat catalogue no longer has (a verb
|
||||
// retired on purpose, such as the delivery seat's mark-ready, ADR 0421), and those verbs, `<seat> <verb>`, once each. A
|
||||
// seat the catalogue does not define (a module's own) keeps every verb, so its loss is still refused. Pure.
|
||||
func WithoutRetired(last []byte) ([]byte, []string) {
|
||||
var m map[string]any
|
||||
if json.Unmarshal(last, &m) != nil {
|
||||
return last, nil
|
||||
}
|
||||
seats, ok := m["seats"].([]any)
|
||||
if !ok {
|
||||
return last, nil
|
||||
}
|
||||
var kept []any
|
||||
var retired []string
|
||||
for _, e := range seats {
|
||||
served, _ := e.(map[string]any)
|
||||
seat, _ := served["seat"].(string)
|
||||
verb, _ := served["verb"].(string)
|
||||
if retiredVerb(seat, verb) {
|
||||
if r := seat + " " + verb; !slices.Contains(retired, r) {
|
||||
retired = append(retired, r)
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, e)
|
||||
}
|
||||
if len(retired) == 0 {
|
||||
return last, nil
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
delete(m, "seats") // as an empty list is written: not at all
|
||||
} else {
|
||||
m["seats"] = kept
|
||||
}
|
||||
out, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return last, nil
|
||||
}
|
||||
sort.Strings(retired)
|
||||
return out, retired
|
||||
}
|
||||
|
||||
// retiredVerb says the seat catalogue defines seat with its verbs and no longer has verb among them. A seat the catalogue
|
||||
// defines without verbs (its holder declares them) retires none.
|
||||
func retiredVerb(seat, verb string) bool {
|
||||
if seat == "" || verb == "" {
|
||||
return false
|
||||
}
|
||||
s, ok := catalogue.SeatNamed(seat)
|
||||
if !ok || len(s.Serves) == 0 {
|
||||
return false
|
||||
}
|
||||
return !slices.ContainsFunc(s.Serves, func(v catalogue.Verb) bool { return v.Name == verb })
|
||||
}
|
||||
|
||||
// sameMembership says two memberships say the same, whatever the order of their fields. Pure.
|
||||
func sameMembership(was, now []byte) bool {
|
||||
var a, b any
|
||||
|
||||
@@ -35,10 +35,12 @@ var States = []string{StateAssertions, StateMemberships, StateTakeover, StateUse
|
||||
const (
|
||||
EventUserList = "user-list-changed"
|
||||
EventMembership = "membership-changed"
|
||||
// EventAsserted is said when the part created a bucket, log or stream a manifest declares (issue 1790).
|
||||
EventAsserted = "asserted"
|
||||
)
|
||||
|
||||
// Emits is every event the part says.
|
||||
var Emits = []string{EventMembership, EventUserList}
|
||||
var Emits = []string{EventAsserted, EventMembership, EventUserList}
|
||||
|
||||
// Shadow is the switch's one state in this milestone: the bus node follows the controller's list.
|
||||
const Shadow = "shadow"
|
||||
@@ -83,21 +85,25 @@ func (s UserListState) inputs() string {
|
||||
|
||||
// Result is what one composition did.
|
||||
type Result struct {
|
||||
UserList UserListState `json:"user_list"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Written bool `json:"written"`
|
||||
Memberships int `json:"memberships"`
|
||||
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"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
PublishedOrAsserted string `json:"published_or_asserted"`
|
||||
UserList UserListState `json:"user_list"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Written bool `json:"written"`
|
||||
Memberships int `json:"memberships"`
|
||||
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"`
|
||||
// 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"`
|
||||
NotAsserted []string `json:"not_asserted,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
PublishedOrAsserted string `json:"published_or_asserted"`
|
||||
}
|
||||
|
||||
// NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships.
|
||||
const NothingTakesEffect = "memberships only (on the assignments stream, a first one or one that only adds to the last published; a loss stays a shadow difference); the user list, assertions and grants stay in shadow until take-over"
|
||||
const NothingTakesEffect = "memberships (on the assignments stream, a first one or one that only adds to the last published or drops only a verb the seat catalogue retired; any other loss stays a shadow difference), and each bucket, log or stream a manifest declares that the bus lacks, created once with the manifest's limits (the operator, 2026-10-26 08:54); the user list, the grants and any change to a stream the bus holds stay in shadow until take-over"
|
||||
|
||||
// Part is the grants part: it reads, calls and writes through these, and nothing else.
|
||||
type Part struct {
|
||||
@@ -107,11 +113,36 @@ type Part struct {
|
||||
Emit Emitter
|
||||
// Publish publishes memberships: the process's connection; nil in the tools, which ask the process instead.
|
||||
Publish Publisher
|
||||
Now func() time.Time
|
||||
// Create creates a declared stream the bus lacks: the process's connection; nil in the tools, which ask the process.
|
||||
Create Creator
|
||||
Now func() time.Time
|
||||
|
||||
cache Manifests
|
||||
mu sync.Mutex
|
||||
last Result
|
||||
made creations
|
||||
// askSeen is the revision of the last ask to compose (KeyAsked) this part acted on.
|
||||
askSeen uint64
|
||||
}
|
||||
|
||||
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
|
||||
// streams again even where they were created at this manifest before.
|
||||
type Ask struct {
|
||||
By string `json:"by"`
|
||||
At time.Time `json:"at"`
|
||||
Assert string `json:"assert,omitempty"`
|
||||
}
|
||||
|
||||
// askedAssert is the module a new ask names to assert by hand, read once per ask; empty when none.
|
||||
func (p *Part) askedAssert() string {
|
||||
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
|
||||
if err != nil || !found || rev == p.askSeen {
|
||||
return ""
|
||||
}
|
||||
p.askSeen = rev
|
||||
var a Ask
|
||||
_ = json.Unmarshal(raw, &a)
|
||||
return a.Assert
|
||||
}
|
||||
|
||||
func (p *Part) now() time.Time {
|
||||
@@ -187,11 +218,25 @@ func (p *Part) Compose() Result {
|
||||
}
|
||||
}
|
||||
held, heldErr := HeldStreams(p.Call)
|
||||
forced := ""
|
||||
if p.Create != nil {
|
||||
forced = p.askedAssert()
|
||||
}
|
||||
for _, module := range sortedKeys(in.Manifests) {
|
||||
if !assigned(in, module) {
|
||||
continue
|
||||
}
|
||||
a := Assert(module, in.Digests[module], in.Manifests[module], held, heldErr)
|
||||
// Created only where the bus was read and lacks it, never changed where it holds it (issue 1790).
|
||||
if p.Create != nil && heldErr == nil {
|
||||
if made := createMissing(p.Create, &p.made, &a, in.Manifests[module], forced == module); len(made) > 0 {
|
||||
res.Asserted = append(res.Asserted, fmt.Sprintf("%s at %s: created %s", module, a.Manifest, strings.Join(made, ", ")))
|
||||
p.say(&res, EventAsserted, map[string]any{"module": module, "manifest": a.Manifest, "created": made})
|
||||
}
|
||||
for _, why := range notCreated(a) {
|
||||
res.NotAsserted = append(res.NotAsserted, module+" "+why)
|
||||
}
|
||||
}
|
||||
res.Assertions++
|
||||
data, _ := json.Marshal(a)
|
||||
if p.changed(&res, StateAssertions, module, data) {
|
||||
|
||||
@@ -62,8 +62,11 @@ func (p *Part) Grants(module string) (any, error) {
|
||||
// MembershipOf answers the tool membership: one module on one node as composed now, and as last published by the
|
||||
// part's process (its state memberships says what became of each at the last pass).
|
||||
func (p *Part) MembershipOf(node, module string) (any, error) {
|
||||
if node == "" && module == "" {
|
||||
return p.Held()
|
||||
}
|
||||
if node == "" || module == "" {
|
||||
return nil, errors.New("membership names a node and a module")
|
||||
return nil, errors.New("membership names a node and a module, or neither for every membership held back")
|
||||
}
|
||||
in := ReadInputs(p.Read, &p.cache)
|
||||
recs, left := Records(in)
|
||||
@@ -90,6 +93,32 @@ func (p *Part) MembershipOf(node, module string) (any, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
raw, rev, found, err := p.Store.Get(StateUserList, KeyPass)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the last pass cannot be read: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return map[string]any{"held": []any{}, "says": "no pass of the part's process has been kept yet"}, nil
|
||||
}
|
||||
var res Result
|
||||
if err := json.Unmarshal(raw, &res); err != nil {
|
||||
return nil, fmt.Errorf("the last pass cannot be read: %w", err)
|
||||
}
|
||||
type held struct {
|
||||
Membership string `json:"membership"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
out := []held{}
|
||||
for _, n := range res.NotPublished {
|
||||
key, why, _ := strings.Cut(n, ": ")
|
||||
out = append(out, held{Membership: key, Why: why})
|
||||
}
|
||||
return map[string]any{"held": out, "count": len(out), "pass": rev, "published": res.Published}, nil
|
||||
}
|
||||
|
||||
// Pass composes and keeps the result as the last pass, for the tools to read.
|
||||
func (p *Part) Pass() Result {
|
||||
res := p.Compose()
|
||||
@@ -108,7 +137,7 @@ const NoGrantAgain = "missing: a verb to ask the controller to write a module's
|
||||
// AskCompose is the tool compose-now: it asks the part's process to compose and publish now (only the process holds the
|
||||
// grant to publish a membership), and answers the last pass, whose revision moves when the process answered.
|
||||
func (p *Part) AskCompose(caller string) (any, error) {
|
||||
ask, _ := json.Marshal(map[string]any{"by": caller, "at": p.now()})
|
||||
ask, _ := json.Marshal(Ask{By: caller, At: p.now()})
|
||||
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the ask could not be written: %w", err)
|
||||
@@ -151,7 +180,9 @@ func (p *Part) UserList() (any, error) {
|
||||
}
|
||||
|
||||
// AssertOne answers the tool assert: one module's buckets, logs and streams as composed from its manifest at the
|
||||
// indexer's trunk, beside what the bus holds. It asserts nothing.
|
||||
// indexer's trunk, beside what the bus holds, and asks the part's process (which alone holds the grant to create) to
|
||||
// create what the bus lacks now, even one it created at this manifest and that is gone since (issue 1790). It changes no
|
||||
// stream the bus holds. Here, in the tools, it creates nothing itself: the answer names the ask.
|
||||
func (p *Part) AssertOne(module string) (any, error) {
|
||||
if module == "" {
|
||||
return nil, errors.New("assert names a module")
|
||||
@@ -172,7 +203,27 @@ func (p *Part) AssertOne(module string) (any, error) {
|
||||
return nil, err
|
||||
}
|
||||
held, heldErr := HeldStreams(p.Call)
|
||||
return Assert(module, v.Manifest, m, held, heldErr), nil
|
||||
a := Assert(module, v.Manifest, m, held, heldErr)
|
||||
out := map[string]any{"assertion": a}
|
||||
missing := 0
|
||||
for _, it := range a.Items {
|
||||
if it.Held == nil {
|
||||
missing++
|
||||
}
|
||||
}
|
||||
if missing == 0 {
|
||||
out["says"] = "the bus holds every bucket, log and stream the manifest declares: nothing to create; a difference is only said"
|
||||
return out, nil
|
||||
}
|
||||
ask, _ := json.Marshal(Ask{By: "grants_assert", At: p.now(), Assert: module})
|
||||
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the ask to create could not be written: %w", err)
|
||||
}
|
||||
out["asked"] = rev
|
||||
out["says"] = fmt.Sprintf("%d the bus lacks: the part's process creates them at this ask if %s is placed (its grant names only placed "+
|
||||
"modules' streams) and says asserted; grants_assert again shows what the bus holds then", missing, module)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Answer is a tool's arguments read as text.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"nats.nats_users"
|
||||
],
|
||||
"emits": [
|
||||
"asserted",
|
||||
"membership-changed",
|
||||
"user-list-changed"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user