Revert L3 (mesh-delivery, merge d827420): its node-engine removed every held module on novox, the bus server among them, at 08:54 CET on 2026-10-29 (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-delivery@c1b4533201c8
mesh/delivery-group group revert/774-l3 merged: every member is merged
mesh/check being carried to 6771db22ae69 on d8274204fbed | from mesh-catalog 1555241393fe | novox/mesh-delivery#109 in group revert/774-l3
mesh/delivery merged: source.main moved to novox/mesh-delivery@c1b4533201c8
mesh/delivery-group group revert/774-l3 merged: every member is merged
mesh/check being carried to 6771db22ae69 on d8274204fbed | from mesh-catalog 1555241393fe | novox/mesh-delivery#109 in group revert/774-l3
L3 lands again with two fixes: a held module is never removed, and the grants cover L3's new subjects.
This commit is contained in:
@@ -214,10 +214,6 @@ func (s ownStore) Put(state, key string, value []byte) (uint64, error) {
|
||||
return s.c.State(state).Put(key, json.RawMessage(value))
|
||||
}
|
||||
|
||||
func (s ownStore) Keys(state string) ([]string, error) { return s.c.State(state).Keys() }
|
||||
|
||||
func (s ownStore) Delete(state, key string) error { return s.c.State(state).Delete(key) }
|
||||
|
||||
// emitting is the process connection as the part's emitter: its events, with no options.
|
||||
type emitting struct{ c *process.Conn }
|
||||
|
||||
|
||||
@@ -18,41 +18,6 @@ type creating struct {
|
||||
js jetstream.JetStream
|
||||
}
|
||||
|
||||
// CreateConsumer creates one durable consumer an account needs, by its stream and name (ADR 0561 §6): the create alone;
|
||||
// one the bus holds answers ErrHeld and is left as it is.
|
||||
func (c creating) CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
||||
defer cancel()
|
||||
if _, err := c.js.Consumer(ctx, stream, cfg.Durable); err == nil {
|
||||
return grants.ErrHeld
|
||||
}
|
||||
_, err := c.js.CreateConsumer(ctx, stream, cfg)
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, jetstream.ErrConsumerExists):
|
||||
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
|
||||
}
|
||||
|
||||
// MatchConsumer brings a held consumer to cfg on a person's ask (grants_consumer_match), keeping where it delivers from:
|
||||
// the server refuses a changed deliver policy, and a consumer moved would replay or lose what it holds.
|
||||
func (c creating) MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
||||
defer cancel()
|
||||
held, err := c.js.Consumer(ctx, stream, cfg.Durable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
have := held.CachedInfo().Config
|
||||
cfg.DeliverPolicy, cfg.OptStartSeq, cfg.OptStartTime = have.DeliverPolicy, have.OptStartSeq, have.OptStartTime
|
||||
_, err = c.js.UpdateConsumer(ctx, stream, cfg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c creating) CreateDeclared(cfg jetstream.StreamConfig) error {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, requestWithin)
|
||||
defer cancel()
|
||||
|
||||
@@ -62,9 +62,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// keptStates is what the part keeps (ADR 0495 §2-§3): each readable, for the view and a session, but the wanted accounts,
|
||||
// by the vault alone (ADR 0561 §2).
|
||||
func keptStates() []announce.State { return grants.Announced() }
|
||||
// keptStates is what the part keeps, each readable (ADR 0495 §2-§3): the view and a session read them.
|
||||
func keptStates() []announce.State {
|
||||
var out []announce.State
|
||||
for _, s := range grants.States {
|
||||
out = append(out, announce.State{Name: s, Readable: true})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// run is the process: it returns when ctx ends, or when it has no credential or cannot dial the bus at all. A bucket or
|
||||
// read the bus refuses never ends it.
|
||||
@@ -112,9 +117,8 @@ 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}, Create: creating{ctx: ctx, js: js}, Consumers: creating{ctx: ctx, js: js},
|
||||
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf,
|
||||
Bus: grants.BusAddress{URL: account.URL, Fingerprint: account.Fingerprint}}
|
||||
Publish: memberships{ctx: ctx, nc: nc, js: js}, Create: creating{ctx: ctx, js: js},
|
||||
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf}
|
||||
f.Watch = bus
|
||||
// The operator's word on take-over and hand-back is their yes on their channel: the part asks it itself, and acts on
|
||||
// the warrant once (mesh-sdk asker), after its refusals.
|
||||
@@ -134,9 +138,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
})
|
||||
}
|
||||
// The controller makes this module's durable consumer on EVENTS from the manifest's consumes (filtered on the two
|
||||
// condition subjects and the vault's account-issued; ADR 0561 §6 gives that to the part from the lease write); it is
|
||||
// pulled and every message acknowledged, so its pending never grows. Where it is not there yet the pass says so and
|
||||
// hears again at the next change. The vault's bus-accounts is watched beside it, so a hash is composed either way.
|
||||
// condition subjects); it is pulled and every message acknowledged, so its pending never grows. Where it is not there
|
||||
// yet the pass says so and hears again at the next change.
|
||||
f.Hear = func(handle func(string, []byte)) error {
|
||||
cons, err := js.Consumer(ctx, "EVENTS", account.Node+"_"+account.Module)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
@@ -40,10 +39,6 @@ func (runtimeStore) Put(state, key string, value []byte) (uint64, error) {
|
||||
return stdio.State(state).Put(key, json.RawMessage(value))
|
||||
}
|
||||
|
||||
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
|
||||
|
||||
func (runtimeStore) Delete(state, key string) error { return stdio.State(state).Delete(key) }
|
||||
|
||||
// runtimeBus calls tools and says events through the runtime.
|
||||
type runtimeBus struct{}
|
||||
|
||||
@@ -118,43 +113,6 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.HandBack(stdio.CallerOf(a), grants.Answer(a, "why"))
|
||||
}},
|
||||
{Name: grants.ToolAccounts, Description: "The bus accounts the mesh wants (novox/hq ADR 0561 §2, §9): every account the vault " +
|
||||
"mints that placement composes (a module's, a process's, a machine's provisioner client's), as the grants part last " +
|
||||
"wrote it to its state accounts, which the vault alone reads: user, kind, node, module, process, the bus's address and " +
|
||||
"fingerprint, a module's claims with the verbs each seat promises, and when the key was written; one user's or all; " +
|
||||
"what the last pass wrote, deleted or kept; and the durable consumers the accounts need as the last pass found them " +
|
||||
"(missing, differing and why, created; whether the lease is handed away). Never a password. Read-only: " +
|
||||
"grants_compose_now writes them again.",
|
||||
Input: schema(map[string]string{"user": "one bus user, e.g. ace.mesh-build-go"}),
|
||||
Run: func(a map[string]any) (any, error) { return p.Accounts(grants.Answer(a, "user")) }},
|
||||
{Name: grants.ToolConsumerMatch, Description: "Bring one durable consumer an account needs (a module's on EVENTS, a process's on " +
|
||||
"its work queue; novox/hq ADR 0561 §6) to the configuration the bus composer composes, by hand: the recovery when " +
|
||||
"the condition mesh-grants.consumer-differs names it. Asks the part's process, which alone may, and keeps where the " +
|
||||
"consumer delivers from. Refused while the controller holds its lease (the consumers are the controller's then) and " +
|
||||
"for a consumer the records do not compose. grants_user_list's last pass says what became of it.",
|
||||
Input: schema(map[string]string{"stream": "the stream, e.g. EVENTS", "consumer": "the consumer, e.g. ace_mesh-build-go"}, "stream", "consumer"),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.MatchConsumer(grants.Answer(a, "stream"), grants.Answer(a, "consumer"))
|
||||
}},
|
||||
{Name: grants.ToolAcceptHashes, Description: "Release the hold of a user list that changes several loaded users' hashes at " +
|
||||
"once (novox/hq ADR 0561 §7; the condition mesh-grants.user-list-held names them), by a person's act with why: names " +
|
||||
"exactly the users held, bound to the held list's digest, so the part's process writes that list once at its next pass " +
|
||||
"and says hashes-accepted. It never releases another hold (a failed read, a user dropped for want of a hash, a lost " +
|
||||
"right), and a list composed otherwise since is held again for a new acceptance.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"users": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the users held, as the condition names them"},
|
||||
"why": map[string]any{"type": "string", "description": "why their hashes change"}}, "required": []string{"users", "why"}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
var users []string
|
||||
if list, ok := a["users"].([]any); ok {
|
||||
for _, u := range list {
|
||||
if s, ok := u.(string); ok {
|
||||
users = append(users, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
return p.AcceptHashes(stdio.CallerOf(a), users, grants.Answer(a, "why"))
|
||||
}},
|
||||
{Name: grants.ToolTakeOverState, Description: "Where the switch stands: the list the bus node follows, the ask open now, the request, and " +
|
||||
"every act with what became of it (asked, switched, declined, expired, refused and why).",
|
||||
Input: schema(nil),
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-grants
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028205404-8eafc0c5ce3c
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028191251-5228a5782d5c
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028205404-8eafc0c5ce3c h1:hcqOCd5q3M0ZiEzB6MhwxT/AOKkm9gaskKqPcpkAAg8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028205404-8eafc0c5ce3c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028191251-5228a5782d5c h1:ngcItoLuh2uP24toZvkCXGA/3+TBrLTwTbE28XpYDWY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028191251-5228a5782d5c/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=
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -58,45 +57,6 @@ type fakeBus struct {
|
||||
refuseUsers bool
|
||||
// rootFree answers the controller's root-free verb: nil is no answerer at all (no responders).
|
||||
rootFree func(machines []string) (any, error)
|
||||
// consumers are the durable consumers the bus holds, by stream, as nats_streams answers them for one stream; and
|
||||
// consumerCreates each consumer the part asked to create, `<stream>.<name>`.
|
||||
consumers map[string][]map[string]any
|
||||
consumerCreates []string
|
||||
}
|
||||
|
||||
// MatchConsumer is the bus's update of a held durable consumer to the configuration given.
|
||||
func (b *fakeBus) MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.consumerCreates = append(b.consumerCreates, "match "+stream+"."+cfg.Durable)
|
||||
for _, c := range b.consumers[stream] {
|
||||
if c["name"] == cfg.Durable {
|
||||
c["max_deliver"] = cfg.MaxDeliver
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("consumer not found")
|
||||
}
|
||||
|
||||
// CreateConsumer is the bus's create of a durable consumer: held when the bus holds one of that name, else held from now.
|
||||
func (b *fakeBus) CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.consumerCreates = append(b.consumerCreates, stream+"."+cfg.Durable)
|
||||
for _, c := range b.consumers[stream] {
|
||||
if c["name"] == cfg.Durable {
|
||||
return grants.ErrHeld
|
||||
}
|
||||
}
|
||||
filters := append([]string{}, cfg.FilterSubjects...)
|
||||
if cfg.FilterSubject != "" {
|
||||
filters = []string{cfg.FilterSubject}
|
||||
}
|
||||
if b.consumers == nil {
|
||||
b.consumers = map[string][]map[string]any{}
|
||||
}
|
||||
b.consumers[stream] = append(b.consumers[stream], map[string]any{"name": cfg.Durable, "filters": filters, "max_deliver": cfg.MaxDeliver})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *fakeBus) LastMembership(node, module string) ([]byte, uint64, bool, error) {
|
||||
@@ -217,9 +177,6 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
|
||||
}
|
||||
return json.Marshal(map[string]any{"users": out})
|
||||
case grants.CallStreams:
|
||||
if stream, _ := args["stream"].(string); stream != "" {
|
||||
return json.Marshal(map[string]any{"detail": []map[string]any{{"name": stream, "consumer_detail": b.consumers[stream]}}})
|
||||
}
|
||||
return json.Marshal(map[string]any{"detail": b.streams})
|
||||
case grants.CallRootFree:
|
||||
if b.rootFree == nil {
|
||||
@@ -260,27 +217,6 @@ func (s ownStore) Put(state, key string, value []byte) (uint64, error) {
|
||||
return b.revs[state+"/"+key], nil
|
||||
}
|
||||
|
||||
func (s ownStore) Keys(state string) ([]string, error) {
|
||||
b := s.b
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
var out []string
|
||||
for k := range b.own[state] {
|
||||
out = append(out, k)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s ownStore) Delete(state, key string) error {
|
||||
b := s.b
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.writes = append(b.writes, state+" delete")
|
||||
delete(b.own[state], key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *fakeBus) Emit(event string, _ any) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -1208,7 +1144,7 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("membership = %v", ms)
|
||||
}
|
||||
gr, _ := p.Grants("mod-x")
|
||||
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesGrants || g["controller_count"] != 1 {
|
||||
if g, _ := gr.(map[string]any); g["writes"] != grants.WritesNoGrant || g["count"] != 1 {
|
||||
t.Errorf("grants = %v", gr)
|
||||
}
|
||||
|
||||
@@ -1435,12 +1371,7 @@ func TestGrants(t *testing.T) {
|
||||
var fresh []asks.Condition
|
||||
fp := partOn(b)
|
||||
fp.Objects = p.Objects
|
||||
fp.Asker, fp.Say = fa, func(c asks.Condition) error {
|
||||
if c.Key != grants.GrantMissingCondition { // its own row below
|
||||
fresh = append(fresh, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
fp.Asker, fp.Say = fa, func(c asks.Condition) error { fresh = append(fresh, c); return nil }
|
||||
fp.Compose()
|
||||
if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != asks.StateCleared {
|
||||
t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh)
|
||||
@@ -1573,7 +1504,7 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, w := range b.writes {
|
||||
if !slices.Contains(grants.States, strings.TrimSuffix(w, " delete")) {
|
||||
if !slices.Contains(grants.States, w) {
|
||||
t.Errorf("wrote %s, which is not the part's own state", w)
|
||||
}
|
||||
}
|
||||
@@ -1783,31 +1714,6 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// 774 (ADR 0563 §2, §3): the node-engine's manifest (mesh-host) is placed on every node though no node is assigned it,
|
||||
// so its declared state acts is created as a placed module's, and composed for the user list's create right.
|
||||
t.Run("assertions: the node-engine's declared state is created though no node is assigned it", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","state":[{"name":"acts"}]}`)
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
p.Create = b
|
||||
p.Compose()
|
||||
if !slices.Contains(b.creates, "KV_"+buscompose.EngineActsBucket) {
|
||||
t.Fatalf("the node-engine's acts were not created: %v", b.creates)
|
||||
}
|
||||
recs, _ := grants.Records(grants.ReadInputs(b, &grants.Manifests{}))
|
||||
if recs.Engine == nil || len(recs.Engine.State) != 1 || recs.Engine.State[0].Bucket() != buscompose.EngineActsBucket {
|
||||
t.Fatalf("the records carry the node-engine as %+v", recs.Engine)
|
||||
}
|
||||
for node, mods := range recs.Assigned {
|
||||
for _, d := range mods {
|
||||
if d.Module == buscompose.EngineModule {
|
||||
t.Errorf("the node-engine is a module of %s, which would give it a module's account", node)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
@@ -1917,463 +1823,6 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("the refusal was not said: %v", logged)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/L3a (novox/hq ADR 0547 §5, ADR 0561 §2): the part writes grants.<module>.<fp> in shadow, once each and only for a
|
||||
// module whose accounts the vault holds a hash for, says grant-written for each, deletes one only when a module has no
|
||||
// account with a hash and every read was made, and the tool says every difference from the controller's.
|
||||
t.Run("grants: written in shadow once each, only for hashed accounts, deleted only when read whole, compared with the controller's", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
fp := "src2:" + strings.Repeat("ab", 32)
|
||||
key := func(module string) string { k, _ := delivery.PartGrantKey(module, fp); return k }
|
||||
hash := func(user string) {
|
||||
b.put(grants.ReadAccounts, user, map[string]any{"user": user, "hash": "$2a$10$x", "generation": 1})
|
||||
}
|
||||
held := func() []string { return slices.Sorted(maps.Keys(b.own[grants.StateGrants])) }
|
||||
// The controller's grant for this fingerprint, beside the stale one the fixture holds for another.
|
||||
delete(b.states[grants.ReadControllerGrant], "mod-x.0123456789ab0123456789ab0123456789ab0123456789ab0123456789ab0123")
|
||||
b.put(grants.ReadControllerGrant, key("mod-x"), delivery.ControllerGrant{V: delivery.Version, Accounts: []string{"a.mod-x"}})
|
||||
hash("a.mod-x")
|
||||
hash("a.nats")
|
||||
res := p.Compose()
|
||||
if res.Grants == nil || !slices.Equal(held(), []string{key("mod-x"), key("nats")}) || len(res.Grants.Written) != 2 {
|
||||
t.Fatalf("grants of the hashed modules: held %v, %+v", held(), res.Grants)
|
||||
}
|
||||
if g, err := delivery.DecodeControllerGrant(b.own[grants.StateGrants][key("mod-x")]); err != nil || !slices.Equal(g.Accounts, []string{"a.mod-x"}) {
|
||||
t.Errorf("the grant of mod-x is %+v, %v", g, err)
|
||||
}
|
||||
said := func() int {
|
||||
n := 0
|
||||
for _, e := range b.events {
|
||||
if e == grants.EventGrantWritten {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
if said() != 2 {
|
||||
t.Errorf("grant-written said %d times for two grants: %v", said(), b.events)
|
||||
}
|
||||
if res := p.Compose(); len(res.Grants.Written) != 0 || said() != 2 {
|
||||
t.Errorf("a second pass wrote %v and said %d", res.Grants.Written, said())
|
||||
}
|
||||
// The tool: equal on mod-x, and nats is the part's alone (the controller's fixture holds none).
|
||||
got, err := p.Grants("")
|
||||
gm, _ := got.(map[string]any)
|
||||
diff, _ := gm["differences"].([]grants.GrantDifference)
|
||||
if err != nil || gm["equal"] != false || len(diff) != 1 || diff[0].Key != key("nats") || diff[0].Kind != "only the part's" {
|
||||
t.Errorf("grants = %+v, %v", got, err)
|
||||
}
|
||||
// A user with a hash later: written once. Without it again: deleted. Unread hashes: nothing deleted.
|
||||
hash("b.mod-y")
|
||||
if res := p.Compose(); !slices.Equal(res.Grants.Written, []string{key("mod-y")}) {
|
||||
t.Errorf("mod-y was granted %v when its account got a hash", res.Grants.Written)
|
||||
}
|
||||
b.refused[grants.ReadAccounts] = true
|
||||
if res := p.Compose(); res.Grants == nil || len(res.Grants.Deleted)+len(res.Grants.Written) != 0 || len(res.Grants.Kept) != 1 || len(held()) != 3 {
|
||||
t.Errorf("a pass that could not read the hashes changed grants: %+v, held %v", res.Grants, held())
|
||||
}
|
||||
delete(b.refused, grants.ReadAccounts)
|
||||
delete(b.states[grants.ReadAccounts], "b.mod-y")
|
||||
if res := p.Compose(); !slices.Equal(res.Grants.Deleted, []string{key("mod-y")}) || len(held()) != 2 {
|
||||
t.Errorf("a module with no hashed account kept its grant: %+v, held %v", res.Grants, held())
|
||||
}
|
||||
})
|
||||
|
||||
// Issue 1126 (C5): a module whose source commit has not moved since a build of the old form, with a done key of the
|
||||
// new form at that commit (link2pay, snake, f1-circuits, txt-game), is granted at the indexer's fingerprint, never
|
||||
// judged by its commit; and a placed module left without a grant is said, never passed over: in the pass, and as the
|
||||
// condition mesh-grants.grant-missing once the part's grants are the ones read, cleared when it has one.
|
||||
t.Run("grants: at the indexer's fingerprint whatever the commit, and a placed module without one is said", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error {
|
||||
if c.Key == grants.GrantMissingCondition {
|
||||
said = append(said, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
fp := "src2:d4afe7a1abab5ccdbef6d8629b2245006a2aec60290f430f7b4756e2930ed60c"
|
||||
b.put(grants.ReadModules, "mod-x", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(xManifest)),
|
||||
Repository: "novox/Link2Pay", Commit: "5d20e8bfbc953c51e8ebe3c671fe883df9c36e15"})
|
||||
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, None2: "its builder's version is not read yet",
|
||||
Manifest: delivery.ManifestDigest([]byte(yManifest)), Repository: "novox/y", Commit: "c"})
|
||||
for _, u := range []string{"a.mod-x", "a.nats", "b.mod-y"} {
|
||||
b.put(grants.ReadAccounts, u, map[string]any{"user": u, "hash": "$2a$10$x", "generation": 1})
|
||||
}
|
||||
res := p.Compose()
|
||||
key, _ := delivery.PartGrantKey("mod-x", fp)
|
||||
if _, held := b.own[grants.StateGrants][key]; !held {
|
||||
t.Errorf("mod-x has no grant at the indexer's fingerprint %s: %+v", fp, res.Grants)
|
||||
}
|
||||
if res.Grants == nil || len(res.Grants.Missing) != 1 || !strings.HasPrefix(res.Grants.Missing[0],
|
||||
"mod-y: no fingerprint of the new form: its builder's version is not read yet") {
|
||||
t.Fatalf("missing = %+v", res.Grants)
|
||||
}
|
||||
if len(said) != 1 || said[0].State != asks.StateRaised || !strings.Contains(said[0].Explanation, "mod-y") {
|
||||
t.Fatalf("said %+v", said)
|
||||
}
|
||||
if p.Compose(); len(said) != 1 {
|
||||
t.Errorf("the same missing grant was said again: %+v", said)
|
||||
}
|
||||
b.put(grants.ReadModules, "mod-y", delivery.Module{V: delivery.Version, FP2: fp, Manifest: delivery.ManifestDigest([]byte(yManifest)),
|
||||
Repository: "novox/y", Commit: "c"})
|
||||
if res := p.Compose(); len(res.Grants.Missing) != 0 || len(said) != 2 || said[1].State != asks.StateCleared {
|
||||
t.Errorf("once mod-y has a grant: missing %v, said %+v", res.Grants.Missing, said)
|
||||
}
|
||||
})
|
||||
|
||||
// The guard of ADR 0549 §6 for this repository, as mesh-catalog's checks/placement is for the catalogue: no module.json
|
||||
// here reads the controller's assignments unless ../assignments-readers names it with why, and that list only shrinks.
|
||||
t.Run("assignments readers: only the named modules read the controller's assignments, and the list only shrinks", func(t *testing.T) {
|
||||
raw, err := os.ReadFile("../assignments-readers")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if f := strings.Fields(line); len(f) > 1 && !strings.HasPrefix(f[0], "#") {
|
||||
allowed[f[0]] = true
|
||||
} else if len(f) == 1 {
|
||||
t.Errorf("../assignments-readers names %s with no why", f[0])
|
||||
}
|
||||
}
|
||||
paths, _ := filepath.Glob("../*/module.json")
|
||||
reads := map[string]bool{}
|
||||
for _, path := range paths {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Module string `json:"module"`
|
||||
Reads []string `json:"reads"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatalf("%s: %v", path, err)
|
||||
}
|
||||
if slices.Contains(m.Reads, grants.ReadAssignments) {
|
||||
reads[m.Module] = true
|
||||
if !allowed[m.Module] {
|
||||
t.Errorf("%s reads %s: read where modules run from the seat mesh-placement's placement (ADR 0549 §3); the lease write "+
|
||||
"is refused while a module reads the assignments (ADR 0549 §6)", m.Module, grants.ReadAssignments)
|
||||
}
|
||||
}
|
||||
}
|
||||
for m := range allowed {
|
||||
if !reads[m] {
|
||||
t.Errorf("../assignments-readers names %s, which no longer reads %s: take it off the list", m, grants.ReadAssignments)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 774 (the L3 review, ADR 0549 §3, §6): once the controller's lease is handed away the assignments are frozen and place
|
||||
// nothing; the seat's placement alone does. A module left only in the stale assignments loses its account and its
|
||||
// grant names no account of it; before the lease write the two are still unioned.
|
||||
t.Run("placement alone once the lease is handed away: a module only in the stale assignments has no account or grant of it", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
p.Bus = grants.BusAddress{URL: "nats://a.mesh:4222", Fingerprint: "ab"}
|
||||
pl, _ := delivery.EncodePlacement(delivery.Placement{Node: "a", Modules: []delivery.PlacedModule{{Module: "nats"}, {Module: "mod-x"}}})
|
||||
b.states[grants.ReadPlacement] = map[string][]byte{"placement.a": pl}
|
||||
for _, u := range []string{"a.mod-x", "a.nats", "b.mod-y"} {
|
||||
b.put(grants.ReadAccounts, u, map[string]any{"user": u, "hash": "$2a$10$x", "generation": 1})
|
||||
}
|
||||
fp := "src2:" + strings.Repeat("ab", 32)
|
||||
key, _ := delivery.PartGrantKey("mod-y", fp)
|
||||
grantOf := func() []string {
|
||||
g, _ := delivery.DecodeControllerGrant(b.own[grants.StateGrants][key])
|
||||
return g.Accounts
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/1/2026-10-28", "epoch": 60})
|
||||
p.Compose()
|
||||
if b.own[grants.StateAccounts]["b.mod-y"] == nil || !slices.Equal(grantOf(), []string{"b.mod-y"}) {
|
||||
t.Fatalf("under the controller's lease the assignments no longer count: account %v, grant %v",
|
||||
b.own[grants.StateAccounts]["b.mod-y"] != nil, grantOf())
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
in := grants.ReadInputs(b, &grants.Manifests{})
|
||||
if len(in.Assigned["b"]) != 0 || !slices.Equal(in.Assigned["a"], []string{"mod-x", "nats"}) || in.BusNode != "a" {
|
||||
t.Errorf("after the lease write the stale assignments still place: %v, bus %q", in.Assigned, in.BusNode)
|
||||
}
|
||||
res := p.Compose()
|
||||
if res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-y"}) || slices.Contains(grantOf(), "b.mod-y") {
|
||||
t.Fatalf("a module only in the stale assignments kept its account or grant: %+v, grant %v", res.Accounts, grantOf())
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC4 (novox/hq ADR 0561 §2): every account the vault mints that the records compose is written to the state
|
||||
// accounts once, with the bus's address and a module's claims, read by the vault alone; a key is deleted when placement
|
||||
// no longer names its user, and never in a pass whose reads failed.
|
||||
t.Run("wanted accounts: written once each, read by the vault alone, retired only when placement drops them", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
p.Bus = grants.BusAddress{URL: "nats://a.mesh:4222", Fingerprint: "ab"}
|
||||
p.Compose()
|
||||
p.Compose()
|
||||
wrote := 0
|
||||
for _, w := range b.writes {
|
||||
if w == grants.StateAccounts {
|
||||
wrote++
|
||||
}
|
||||
}
|
||||
held := slices.Sorted(maps.Keys(b.own[grants.StateAccounts]))
|
||||
if !slices.Equal(held, []string{"a.mod-x", "a.nats", "b.mod-y"}) || wrote != 3 {
|
||||
t.Fatalf("accounts %v written %d times in two passes, want the three modules' once each", held, wrote)
|
||||
}
|
||||
nats, err := buscompose.ReadWantedAccount("a.nats", b.own[grants.StateAccounts]["a.nats"])
|
||||
if err != nil || nats.URL != "nats://a.mesh:4222" || nats.Fingerprint != "ab" || len(nats.Claims) != 1 ||
|
||||
nats.Claims[0].Seat != catalogue.BrokerSeat || nats.At.IsZero() {
|
||||
t.Errorf("a.nats is wanted as %+v (%v)", nats, err)
|
||||
}
|
||||
var readers []string
|
||||
for _, s := range grants.Announced() {
|
||||
if s.Name == grants.StateAccounts {
|
||||
if s.Readable {
|
||||
t.Error("the wanted accounts are announced readable by anyone")
|
||||
}
|
||||
readers = s.Readers
|
||||
}
|
||||
}
|
||||
if !slices.Equal(readers, []string{"mesh-vault"}) || buscompose.ReaderMay("mesh-delivery-view", buscompose.GrantsAccounts) {
|
||||
t.Errorf("the wanted accounts are read by %v", readers)
|
||||
}
|
||||
// A read that fails retires nothing; once read, the user placement dropped is retired.
|
||||
delete(b.states[grants.ReadAssignments], "b.mod-y")
|
||||
b.refused[grants.ReadResolved] = true
|
||||
if res := p.Compose(); res.Accounts == nil || len(res.Accounts.Deleted) != 0 || b.own[grants.StateAccounts]["b.mod-y"] == nil {
|
||||
t.Fatalf("a pass whose read failed deleted %+v", res.Accounts)
|
||||
}
|
||||
delete(b.refused, grants.ReadResolved)
|
||||
if res := p.Compose(); res.Accounts == nil || !slices.Equal(res.Accounts.Deleted, []string{"b.mod-y"}) {
|
||||
t.Fatalf("the user placement dropped is not retired: %+v", res.Accounts)
|
||||
}
|
||||
// The tool reads them, one and all, each with when it was written.
|
||||
one, err := p.Accounts("a.nats")
|
||||
all, err2 := p.Accounts("")
|
||||
if err != nil || err2 != nil || one.(map[string]any)["count"] != 1 || all.(map[string]any)["count"] != 2 ||
|
||||
one.(map[string]any)["accounts"].([]buscompose.WantedAccount)[0].At.IsZero() {
|
||||
t.Errorf("the tool reads %v and %v (%v %v)", one, all, err, err2)
|
||||
}
|
||||
if _, err := p.Accounts("b.mod-y"); err == nil {
|
||||
t.Error("the tool reads an account no longer wanted")
|
||||
}
|
||||
p.Pass()
|
||||
if all, _ := p.Accounts(""); all.(map[string]any)["consumers"] == nil {
|
||||
t.Errorf("the tool does not say the account consumers in shadow: %v", all)
|
||||
}
|
||||
// With no bus address no account is written, and the pass says why.
|
||||
bare := partOn(fixture())
|
||||
if res := bare.Compose(); res.Accounts != nil || !strings.Contains(strings.Join(res.Errors, " "), "no bus address") {
|
||||
t.Errorf("a part with no bus address wrote %+v: %v", res.Accounts, res.Errors)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC5 (novox/hq ADR 0561 §1): the vault's account-issued wakes a pass, so the next list carries the new user's hash
|
||||
// within one pass; an event of another kind does not.
|
||||
t.Run("the vault's account-issued composes the list again", func(t *testing.T) {
|
||||
b := fixture()
|
||||
var handle func(string, []byte)
|
||||
p := partOn(b)
|
||||
f := &grants.Follower{Part: p, Watch: &fakeWatcher{opened: make(chan string, 16)}, Logf: func(string, ...any) {},
|
||||
Hear: func(h func(string, []byte)) error { handle = h; return nil }}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() { f.Run(ctx); close(done) }()
|
||||
defer func() { cancel(); <-done }()
|
||||
waitFor(t, func() bool { return b.count(grants.CallUsers) > 0 && handle != nil })
|
||||
held := func() bool {
|
||||
for _, c := range p.Last().UserList.Credentials.Users {
|
||||
if c.User == "a.mod-x" {
|
||||
return c.HashHeld
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if held() {
|
||||
t.Fatal("a.mod-x has a hash before the vault made one")
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.states[grants.ReadAccounts] = map[string][]byte{"a.mod-x": []byte(`{"user":"a.mod-x","hash":"$2a$10$x","generation":1}`)}
|
||||
b.mu.Unlock()
|
||||
handle(buscompose.AccountIssuedSubject, []byte(`{"user":"a.mod-x","kind":"module","node":"a","module":"mod-x","generation":1}`))
|
||||
waitFor(t, held)
|
||||
if !slices.Contains(grants.Consumes, grants.EventAccountIssued) || !grants.IsAccountIssued(grants.EventAccountIssued) ||
|
||||
grants.IsAccountIssued("mesh.mod.mesh-vault.event.secret-changed") {
|
||||
t.Errorf("the part hears %v", grants.Consumes)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC6 (novox/hq ADR 0561 §6): the durable consumers an account needs, with the controller's configuration, are
|
||||
// compared in shadow while the controller holds the lease, and created, only where the bus lacks them, from the write
|
||||
// that hands the lease away; one the bus holds is never changed.
|
||||
t.Run("account consumers: compared while the controller holds the lease, created once it is handed away", func(t *testing.T) {
|
||||
const cManifest = `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["mod-y.done"]}`
|
||||
b := fixture()
|
||||
b.manifestOf("mod-c", cManifest)
|
||||
b.assign("b", "mod-c")
|
||||
b.loaded = shadowOf(t, b)
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
|
||||
p := partOn(b)
|
||||
p.Consumers = b
|
||||
res := p.Compose()
|
||||
if res.Consumers == nil || res.Consumers.On || !slices.Contains(res.Consumers.Missing, "EVENTS.b_mod-c") || len(b.consumerCreates) != 0 {
|
||||
t.Fatalf("while the controller holds the lease: %+v, created %v", res.Consumers, b.consumerCreates)
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
b.consumers = map[string][]map[string]any{"EVENTS": {{"name": "b_other", "filters": []string{"x"}, "max_deliver": 5}}}
|
||||
res = p.Compose()
|
||||
if !res.Consumers.On || !slices.Equal(res.Consumers.Created, []string{"EVENTS.b_mod-c"}) {
|
||||
t.Fatalf("once handed away: %+v", res.Consumers)
|
||||
}
|
||||
var made map[string]any
|
||||
for _, c := range b.consumers["EVENTS"] {
|
||||
if c["name"] == "b_mod-c" {
|
||||
made = c
|
||||
}
|
||||
}
|
||||
if made == nil || made["max_deliver"] != 5 || !slices.Contains(made["filters"].([]string), "mesh.mod.mod-y.event.done") {
|
||||
t.Errorf("b_mod-c was made as %v, not as the controller makes it", made)
|
||||
}
|
||||
res = p.Compose()
|
||||
if len(res.Consumers.Created) != 0 || len(res.Consumers.Missing) != 0 || len(b.consumerCreates) != 1 {
|
||||
t.Errorf("a consumer the bus holds was asked again: %+v, %v", res.Consumers, b.consumerCreates)
|
||||
}
|
||||
// A held consumer that differs is never changed by a pass: it is raised as a condition naming it, and brought to
|
||||
// the composed configuration only by hand (grants_consumer_match), never while the controller holds the lease.
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error {
|
||||
if c.Key != grants.GrantMissingCondition { // its own row
|
||||
said = append(said, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
made["max_deliver"] = 9
|
||||
res = p.Compose()
|
||||
if len(res.Consumers.Differ) != 1 || !strings.Contains(res.Consumers.Differ[0], "EVENTS.b_mod-c: max deliver 9") ||
|
||||
len(said) != 1 || said[0].Key != grants.ConsumerDiffersCondition || said[0].State != asks.StateRaised ||
|
||||
!strings.Contains(said[0].Explanation, "EVENTS.b_mod-c") {
|
||||
t.Fatalf("a differing consumer: %+v, said %+v", res.Consumers, said)
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"instance": "novox/2/2026-10-27", "epoch": 58})
|
||||
if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") {
|
||||
t.Errorf("matched by hand under the controller's lease: %v", err)
|
||||
}
|
||||
b.put(grants.ReadLease, grants.LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
if _, err := p.MatchConsumer("EVENTS", "b_nothing"); err == nil {
|
||||
t.Error("a consumer the records do not compose was asked to match")
|
||||
}
|
||||
if _, err := p.MatchConsumer("EVENTS", "b_mod-c"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res = p.Compose()
|
||||
if !slices.Contains(b.consumerCreates, "match EVENTS.b_mod-c") || made["max_deliver"] != 5 || len(res.Consumers.Matched) != 1 {
|
||||
t.Errorf("the ask to match: %+v, %v", res.Consumers, b.consumerCreates)
|
||||
}
|
||||
res = p.Compose()
|
||||
if len(res.Consumers.Differ) != 0 || said[len(said)-1].State != asks.StateCleared {
|
||||
t.Errorf("once matched: %+v, said %+v", res.Consumers, said)
|
||||
}
|
||||
})
|
||||
|
||||
// 774/AC13 (novox/hq ADR 0561 §7): after take-over a list in which more than one loaded user's hash changes, with no
|
||||
// rotation by the vault naming each, is held: the list in force stays, user-list-held is raised naming those users. One
|
||||
// user's change, or changes the vault's rotation names, are written.
|
||||
t.Run("a list changing several loaded users' hashes is held unless the vault's rotation names each", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
hash := func(user, h string, generation int, rotating bool) {
|
||||
v := map[string]any{"user": user, "hash": h, "generation": generation, "fingerprint": fmt.Sprintf("sha256:%d", generation)}
|
||||
if rotating {
|
||||
v["rotating"] = map[string]any{"login": user + "--b", "generation": generation, "revision": 7}
|
||||
}
|
||||
b.put(grants.ReadAccounts, user, v)
|
||||
}
|
||||
for _, u := range shadowOf(t, b) {
|
||||
hash(u.User, "$2a$10$first", 1, false)
|
||||
}
|
||||
busKey, err := seal.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.put(grants.ReadHave, "a.nats", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
||||
UserList: &delivery.UserListWritten{Source: "a.nats.src2", Revision: 7, Digest: "sha256:aa", Users: 6, Key: busKey.PublicKey}})
|
||||
p := partOn(b)
|
||||
p.Objects = &fakeObjects{}
|
||||
var said []asks.Condition
|
||||
p.Say = func(c asks.Condition) error { said = append(said, c); return nil }
|
||||
sw, _ := json.Marshal(grants.Switch{V: 1, State: grants.TookOver, Acts: []grants.Act{}})
|
||||
if _, err := (ownStore{b}).Put(grants.StateTakeover, grants.KeySwitch, sw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.Compose()
|
||||
inForce, _, _ := p.Sealed()
|
||||
if inForce.Digest == "" {
|
||||
t.Fatal("no list written after take-over")
|
||||
}
|
||||
// One user's hash changes: written.
|
||||
hash("a.mod-x", "$2a$10$second", 2, false)
|
||||
p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest {
|
||||
t.Fatal("a list changing one loaded user's hash was not written")
|
||||
}
|
||||
inForce, _, _ = p.Sealed()
|
||||
// Two change at once, no rotation names them: held, said naming both.
|
||||
hash("a.mod-x", "$2a$10$third", 3, false)
|
||||
hash("a.nats", "$2a$10$third", 2, false)
|
||||
said = nil
|
||||
res := p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "a.mod-x, a.nats") {
|
||||
t.Fatalf("a list changing two loaded users' hashes: %+v", res.Sealed)
|
||||
}
|
||||
if len(said) != 1 || said[0].Key != grants.HeldCondition || said[0].State != asks.StateRaised || !strings.Contains(said[0].Summary, "a.mod-x, a.nats") {
|
||||
t.Errorf("the hold was said as %+v", said)
|
||||
}
|
||||
// The hold is its own guard, beside take-over's refusals (ADR 0562): the switch stays taken over.
|
||||
if p.SwitchState() != grants.TookOver {
|
||||
t.Errorf("the hold moved the switch to %s", p.SwitchState())
|
||||
}
|
||||
// A person accepts exactly the named users' changes of the held list, with why: refused for another set, with no
|
||||
// why, or once the list changed since; accepted, the next pass writes it once, said as a hand act.
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x"}, "rotated by hand"); err == nil || !strings.Contains(err.Error(), "a.mod-x, a.nats") {
|
||||
t.Errorf("an acceptance of another set than the held one: %v", err)
|
||||
}
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, ""); err == nil {
|
||||
t.Error("an acceptance with no why")
|
||||
}
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.nats", "a.mod-x"}, "the vault's store was restored"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The list changed since the acceptance (a.mod-x's hash again): the acceptance is bound to the held list, refused.
|
||||
hash("a.mod-x", "$2a$10$fourth", 4, false)
|
||||
res = p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest != inForce.Digest || res.Sealed == nil || !strings.Contains(res.Sealed.Why, "held") {
|
||||
t.Fatalf("an acceptance of a list that changed since released it: %+v", res.Sealed)
|
||||
}
|
||||
if _, err := p.AcceptHashes("person.jochen", []string{"a.mod-x", "a.nats"}, "the vault's store was restored"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.events = nil
|
||||
p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared ||
|
||||
!slices.Contains(b.events, grants.EventHashesAccepted) {
|
||||
t.Fatalf("an accepted hold was not released once and said: %+v, events %v", said, b.events)
|
||||
}
|
||||
inForce, _, _ = p.Sealed()
|
||||
// The vault's rotation names each: written, the warning cleared.
|
||||
hash("a.mod-x", "$2a$10$fifth", 5, true)
|
||||
hash("a.nats", "$2a$10$fifth", 3, true)
|
||||
p.Compose()
|
||||
if now, _, _ := p.Sealed(); now.Digest == inForce.Digest || said[len(said)-1].State != asks.StateCleared {
|
||||
t.Errorf("changes the vault's rotation names were held: %+v", said)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type fakeAsker struct {
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// **The accounts the mesh wants, said to the vault** (novox/hq ADR 0561 §2, issue 774). The bus composer derives every
|
||||
// user from placement, the manifests and the wants; for each account the vault mints (a module's, a process's, a
|
||||
// machine's provisioner client's), the part writes one key of its state `accounts`, keyed by the bus user, with the
|
||||
// fields the controller's credential carries beside the password. A key is written only when it differs, and deleted
|
||||
// only when placement no longer names its user and every input of the pass was read: a refused read never retires an
|
||||
// account. The vault alone reads the state (buscompose.NamedReaders); it mints each user it holds no value for.
|
||||
|
||||
// StateAccounts is the part's state of wanted accounts.
|
||||
const StateAccounts = buscompose.AccountsState
|
||||
|
||||
// AccountsReaders are the only modules that may read StateAccounts.
|
||||
var AccountsReaders = buscompose.NamedReaders[buscompose.GrantsAccounts]
|
||||
|
||||
// Announced is what the part says it keeps: every state readable by any reader, but the wanted accounts, by the vault
|
||||
// alone (ADR 0495 §2-§3, ADR 0561 §2).
|
||||
func Announced() []announce.State {
|
||||
var out []announce.State
|
||||
for _, s := range States {
|
||||
if s == StateAccounts {
|
||||
out = append(out, announce.State{Name: s, Readers: slices.Clone(AccountsReaders)})
|
||||
continue
|
||||
}
|
||||
out = append(out, announce.State{Name: s, Readable: true})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BusAddress is where every node on the private network reaches the bus, and its certificate's fingerprint: the part's
|
||||
// own credential says both, since the controller gave every node's account the bus holder's overlay name (its
|
||||
// brokerReachableAt).
|
||||
type BusAddress struct {
|
||||
URL string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address. A
|
||||
// node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
|
||||
func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[string]buscompose.WantedAccount, error) {
|
||||
principals, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]buscompose.WantedAccount{}
|
||||
for _, p := range principals {
|
||||
w := buscompose.WantedAccount{User: p.Username(), Kind: p.Kind, Node: p.Node, Module: p.Module, Process: p.Process,
|
||||
URL: bus.URL, Fingerprint: bus.Fingerprint}
|
||||
switch p.Kind {
|
||||
case buscompose.KindModule, buscompose.KindNodeTools:
|
||||
if m, ok := manifestOn(in, p.Node, p.Module); ok {
|
||||
w.Claims = ClaimsOf(m)
|
||||
}
|
||||
case buscompose.KindProcess, buscompose.KindProvisioner:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
out[w.User] = w
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// manifestOn is the manifest a node runs of module: the build its want names, else the trunk's.
|
||||
func manifestOn(in Inputs, node, module string) (catalogue.Manifest, bool) {
|
||||
if m, ok := in.Running[node+"."+module]; ok {
|
||||
return m, true
|
||||
}
|
||||
m, ok := in.Manifests[module]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
// ClaimsOf is a module's claims as its credential carries them: the controller's claimsFor, over the mesh's own seats
|
||||
// (its seat table is seeded from them): each claim's seat and scope, and the verbs the runtime serves for it, the claim's
|
||||
// own or every verb the seat promises; a seat the mesh does not define keeps the claim's scope and no verbs.
|
||||
func ClaimsOf(m catalogue.Manifest) []buscompose.ClaimedSeat {
|
||||
if len(m.Claims) == 0 {
|
||||
return nil
|
||||
}
|
||||
byName := map[string]catalogue.Seat{}
|
||||
for _, s := range catalogue.DefaultSeats() {
|
||||
byName[s.Name] = s
|
||||
}
|
||||
var out []buscompose.ClaimedSeat
|
||||
for _, c := range m.Claims {
|
||||
claimed := buscompose.ClaimedSeat{Seat: c.Name, Scope: c.At()}
|
||||
if s, known := byName[c.Name]; known {
|
||||
claimed.Scope = s.Scope
|
||||
claimed.Serves = c.ServesFor(catalogue.Manifest{Tools: catalogue.VerbNames(s.Serves)})
|
||||
}
|
||||
out = append(out, claimed)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Accounts answers the tool accounts (ADR 0561 §9): the wanted accounts as the part last wrote them, one user's or all,
|
||||
// each with when it was written; and what the last pass did to them. Never a password: the state holds none.
|
||||
func (p *Part) Accounts(user string) (any, error) {
|
||||
users := []string{user}
|
||||
if user == "" {
|
||||
keys, err := p.Store.Keys(StateAccounts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("its own %s cannot be listed: %w", StateAccounts, err)
|
||||
}
|
||||
users = keys
|
||||
}
|
||||
out := []buscompose.WantedAccount{}
|
||||
for _, u := range users {
|
||||
raw, _, found, err := p.Store.Get(StateAccounts, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateAccounts, u, err)
|
||||
}
|
||||
if !found {
|
||||
if user != "" {
|
||||
return nil, fmt.Errorf("no account %s is wanted: placement composes no module, process or provisioner client of that user", user)
|
||||
}
|
||||
continue
|
||||
}
|
||||
w, err := buscompose.ReadWantedAccount(u, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, w)
|
||||
}
|
||||
answer := map[string]any{"accounts": out, "count": len(out), "read_by": AccountsReaders}
|
||||
if raw, _, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
|
||||
var res Result
|
||||
if json.Unmarshal(raw, &res) == nil && res.Accounts != nil {
|
||||
answer["last_pass"] = res.Accounts
|
||||
}
|
||||
// And the durable consumers the accounts need, as the last pass found them: missing and differing ones are read
|
||||
// here in shadow too, where no condition is raised (ADR 0561 §6, §9).
|
||||
if res.Consumers != nil {
|
||||
answer["consumers"] = res.Consumers
|
||||
}
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// AccountsWritten is what one pass did to the state of wanted accounts.
|
||||
type AccountsWritten struct {
|
||||
Wanted int `json:"wanted"`
|
||||
Written []string `json:"written,omitempty"`
|
||||
Deleted []string `json:"deleted,omitempty"`
|
||||
// Kept are keys placement no longer names that this pass did not delete, with why.
|
||||
Kept []string `json:"kept,omitempty"`
|
||||
}
|
||||
|
||||
// writeAccounts writes each wanted account whose key differs, and deletes the key of a user no longer placed, but only
|
||||
// when every input of the pass was read.
|
||||
func (p *Part) writeAccounts(res *Result, in Inputs, recs buscompose.Records) {
|
||||
if p.Bus.URL == "" {
|
||||
res.Errors = append(res.Errors, "the wanted accounts are not written: the part knows no bus address to give them (its own credential names none)")
|
||||
return
|
||||
}
|
||||
wanted, err := WantedAccounts(in, recs, p.Bus)
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, "the wanted accounts could not be composed: "+err.Error())
|
||||
return
|
||||
}
|
||||
done := &AccountsWritten{Wanted: len(wanted)}
|
||||
res.Accounts = done
|
||||
for _, user := range sortedKeys(wanted) {
|
||||
w := wanted[user]
|
||||
if raw, _, found, err := p.Store.Get(StateAccounts, user); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateAccounts, user, err))
|
||||
continue
|
||||
} else if found {
|
||||
if was, err := buscompose.ReadWantedAccount(user, raw); err == nil && was.Same(w) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
w.At = p.now()
|
||||
data, _ := json.Marshal(w)
|
||||
if _, err := p.Store.Put(StateAccounts, user, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateAccounts, user, err))
|
||||
continue
|
||||
}
|
||||
done.Written = append(done.Written, user)
|
||||
}
|
||||
held, err := p.Store.Keys(StateAccounts)
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be listed, so no key of a user gone is deleted: %v", StateAccounts, err))
|
||||
return
|
||||
}
|
||||
failed := in.ReadFailures()
|
||||
placed := Placed(in)
|
||||
for _, user := range held {
|
||||
if _, still := wanted[user]; still {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case placed(user):
|
||||
done.Kept = append(done.Kept, user+": placement still names it (composed with no account this pass)")
|
||||
continue
|
||||
case len(failed) > 0:
|
||||
done.Kept = append(done.Kept, user+": a read of this pass failed, so no account is retired")
|
||||
continue
|
||||
}
|
||||
if err := p.Store.Delete(StateAccounts, user); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be deleted: %v", StateAccounts, user, err))
|
||||
continue
|
||||
}
|
||||
done.Deleted = append(done.Deleted, user)
|
||||
}
|
||||
sort.Strings(done.Kept)
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
)
|
||||
|
||||
// **The durable consumers an account needs** (novox/hq ADR 0561 §6, issue 774). The controller creates a module's
|
||||
// consumer on EVENTS and each process's on its work queue when it issues the account. From the write that hands its
|
||||
// lease away the grants part creates them, with the configuration the bus composer derives (buscompose.AccountConsumers),
|
||||
// only where the bus lacks one; one the bus holds is never changed or deleted, a difference is only said. Before that
|
||||
// write they are compared in shadow alone, so two parts never create one consumer.
|
||||
|
||||
// ReadLease is the controller's lease as the part reads it (buscompose.NamedReaders names the vault and the part), and
|
||||
// LeaseKey its one key.
|
||||
const (
|
||||
ReadLease = buscompose.ControllerLease
|
||||
LeaseKey = buscompose.ControllerLeaseKey
|
||||
)
|
||||
|
||||
// ConsumerCreator creates one durable consumer the bus does not hold: the part's process alone, whose account is granted
|
||||
// the create of each account's consumer by stream and name. One the bus holds answers ErrHeld.
|
||||
type ConsumerCreator interface {
|
||||
CreateConsumer(stream string, cfg jetstream.ConsumerConfig) error
|
||||
// MatchConsumer brings a held consumer to cfg, keeping where it delivers from: only on a person's ask
|
||||
// (grants_consumer_match), never by a pass.
|
||||
MatchConsumer(stream string, cfg jetstream.ConsumerConfig) error
|
||||
}
|
||||
|
||||
// ConsumersState is what one pass found and did of the account consumers.
|
||||
type ConsumersState struct {
|
||||
// On says the lease is handed away, so the part creates what the bus lacks; Why says why it is on or off.
|
||||
On bool `json:"on"`
|
||||
Why string `json:"why"`
|
||||
// Wanted is how many the records compose; Missing those the bus lacks (`<stream>.<name>`), Differ those it holds
|
||||
// otherwise, each with how.
|
||||
Wanted int `json:"wanted"`
|
||||
Missing []string `json:"missing,omitempty"`
|
||||
Differ []string `json:"differ,omitempty"`
|
||||
Created []string `json:"created,omitempty"`
|
||||
// Matched is a consumer brought to the composed configuration on a person's ask, or why not.
|
||||
Matched []string `json:"matched,omitempty"`
|
||||
NotCreated []string `json:"not_created,omitempty"`
|
||||
// Unread is a stream whose consumers could not be read: nothing on it is compared or created.
|
||||
Unread []string `json:"unread,omitempty"`
|
||||
}
|
||||
|
||||
// heldConsumer is one consumer as the bus module's nats_streams reads it for one stream.
|
||||
type heldConsumer struct {
|
||||
Name string `json:"name"`
|
||||
Filters []string `json:"filters"`
|
||||
MaxDeliver int `json:"max_deliver"`
|
||||
}
|
||||
|
||||
// heldConsumers reads the consumers the bus holds on stream, by name.
|
||||
func heldConsumers(c Caller, stream string) (map[string]heldConsumer, error) {
|
||||
raw, err := c.Call(CallStreams, map[string]any{"stream": stream})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %w", CallStreams, stream, err)
|
||||
}
|
||||
var got struct {
|
||||
Detail []struct {
|
||||
Name string `json:"name"`
|
||||
Consumers []heldConsumer `json:"consumer_detail"`
|
||||
} `json:"detail"`
|
||||
}
|
||||
if err := json.Unmarshal(unwrap(raw), &got); err != nil {
|
||||
return nil, fmt.Errorf("%s %s answered no consumers: %w", CallStreams, stream, err)
|
||||
}
|
||||
out := map[string]heldConsumer{}
|
||||
for _, d := range got.Detail {
|
||||
if d.Name != stream {
|
||||
continue
|
||||
}
|
||||
for _, c := range d.Consumers {
|
||||
out[c.Name] = c
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// leaseHandedAway reads the lease as the part's reads give it.
|
||||
func leaseHandedAway(r Reader) (bool, string) {
|
||||
raw, found, err := r.Get(ReadLease, LeaseKey)
|
||||
return buscompose.LeaseHandedAway(raw, found, err)
|
||||
}
|
||||
|
||||
// consumers compares the account consumers the records compose with those the bus holds, and creates the missing ones
|
||||
// once the lease is handed away and the part holds a creator.
|
||||
func (p *Part) consumers(res *Result, recs buscompose.Records, match string) {
|
||||
users, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, "the account consumers could not be composed: "+err.Error())
|
||||
return
|
||||
}
|
||||
st := &ConsumersState{}
|
||||
st.On, st.Why = leaseHandedAway(p.Read)
|
||||
res.Consumers = st
|
||||
wanted := buscompose.AccountConsumers(users)
|
||||
st.Wanted = len(wanted)
|
||||
byStream := map[string][]buscompose.Consumer{}
|
||||
for _, mc := range wanted {
|
||||
byStream[mc.Consumer.Stream] = append(byStream[mc.Consumer.Stream], mc.Consumer)
|
||||
}
|
||||
for _, stream := range sortedKeys(byStream) {
|
||||
held, err := heldConsumers(p.Call, stream)
|
||||
if err != nil {
|
||||
st.Unread = append(st.Unread, stream+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
for _, c := range byStream[stream] {
|
||||
key := stream + "." + c.Name
|
||||
h, ok := held[c.Name]
|
||||
if ok {
|
||||
why := consumerDiffers(c, h)
|
||||
if why != "" && key == match && st.On && p.Consumers != nil {
|
||||
if err := p.Consumers.MatchConsumer(stream, buscompose.ConsumerConfig(c)); err != nil {
|
||||
st.Matched = append(st.Matched, key+": not matched: "+err.Error())
|
||||
} else {
|
||||
st.Matched = append(st.Matched, key+": matched, asked by hand")
|
||||
continue
|
||||
}
|
||||
}
|
||||
if why != "" {
|
||||
st.Differ = append(st.Differ, key+": "+why)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !st.On || p.Consumers == nil {
|
||||
st.Missing = append(st.Missing, key)
|
||||
continue
|
||||
}
|
||||
switch err := p.Consumers.CreateConsumer(stream, buscompose.ConsumerConfig(c)); {
|
||||
case err == nil:
|
||||
st.Created = append(st.Created, key)
|
||||
p.say(res, EventAsserted, map[string]any{"consumer": c.Name, "stream": stream})
|
||||
case errors.Is(err, ErrHeld):
|
||||
default:
|
||||
st.Missing = append(st.Missing, key)
|
||||
st.NotCreated = append(st.NotCreated, key+": "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
p.sayDiffer(st)
|
||||
}
|
||||
|
||||
// sayDiffer raises the warning condition mesh-grants.consumer-differs while an account consumer the part owns (from the
|
||||
// lease write) differs from its composed configuration, naming each with the fields that differ, and clears it when they
|
||||
// match. Said once per set of differences, again when it changes. Before the lease write the consumers are the
|
||||
// controller's, and a difference is only said in the pass.
|
||||
func (p *Part) sayDiffer(st *ConsumersState) {
|
||||
if p.Say == nil {
|
||||
return
|
||||
}
|
||||
why := ""
|
||||
if st.On && len(st.Differ) > 0 {
|
||||
why = strings.Join(st.Differ, "; ")
|
||||
}
|
||||
p.holdMu.Lock()
|
||||
defer p.holdMu.Unlock()
|
||||
if p.differKnown && why == p.differSaid {
|
||||
return
|
||||
}
|
||||
c := asks.Condition{Key: ConsumerDiffersCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
||||
if why == "" {
|
||||
c.State = asks.StateCleared
|
||||
} else {
|
||||
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
||||
c.Summary = fmt.Sprintf("%d durable consumers differ from their composed configuration", len(st.Differ))
|
||||
c.Headline, c.Needs = "A module's consumer is not as composed", "a look, and grants_consumer_match for each"
|
||||
c.Explanation = asks.NeedsYou + "a look. The bus holds these consumers otherwise than the bus composer composes them, " +
|
||||
"and the grants part never changes a held one by itself (its delivery position would be lost): " + why
|
||||
c.Resolved = "every account consumer matches, by grants_consumer_match or otherwise"
|
||||
}
|
||||
if p.Say(c) == nil {
|
||||
p.differSaid, p.differKnown = why, true
|
||||
}
|
||||
}
|
||||
|
||||
// ConsumerDiffersCondition is the key of the warning raised while an account consumer differs (ADR 0561 §6).
|
||||
const ConsumerDiffersCondition = "mesh-grants.consumer-differs"
|
||||
|
||||
// MatchConsumer is the tool consumer-match (ADR 0561 §6, §9): it asks the part's process to bring one account consumer
|
||||
// the records compose to its composed configuration, keeping where it delivers from. Refused while the controller holds
|
||||
// the lease (the consumers are the controller's then), and for a consumer the records do not compose.
|
||||
func (p *Part) MatchConsumer(stream, name string) (any, error) {
|
||||
if stream == "" || name == "" {
|
||||
return nil, errors.New("consumer-match names a stream and a consumer")
|
||||
}
|
||||
if on, why := leaseHandedAway(p.Read); !on {
|
||||
return nil, fmt.Errorf("not asked: %s; the controller makes and matches the consumers while it holds the lease", why)
|
||||
}
|
||||
in := ReadInputs(p.Read, &p.cache)
|
||||
recs, _ := Records(in)
|
||||
users, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
found := false
|
||||
for _, mc := range buscompose.AccountConsumers(users) {
|
||||
if mc.Consumer.Stream == stream && mc.Consumer.Name == name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("the records compose no account consumer %s on %s: the part brings only those to their configuration", name, stream)
|
||||
}
|
||||
ask, _ := json.Marshal(Ask{By: "grants_consumer_match", At: p.now(), Consumer: stream + "." + name})
|
||||
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the ask could not be written: %w", err)
|
||||
}
|
||||
return map[string]any{"asked": rev, "says": "the part's process brings " + stream + "." + name + " to its composed " +
|
||||
"configuration at this ask, keeping where it delivers from; the last pass's consumers.matched says what became of it"}, nil
|
||||
}
|
||||
|
||||
// consumerDiffers says how a held consumer differs from the composed one in what nats_streams reads back, or "".
|
||||
func consumerDiffers(c buscompose.Consumer, h heldConsumer) string {
|
||||
var why []string
|
||||
want, have := slices.Sorted(slices.Values(c.Filters)), slices.Sorted(slices.Values(h.Filters))
|
||||
if !slices.Equal(want, have) {
|
||||
why = append(why, fmt.Sprintf("filters %v, composed %v", have, want))
|
||||
}
|
||||
maxDeliver := c.MaxDeliver
|
||||
if maxDeliver == 0 {
|
||||
maxDeliver = -1
|
||||
}
|
||||
if h.MaxDeliver != maxDeliver {
|
||||
why = append(why, fmt.Sprintf("max deliver %d, composed %d", h.MaxDeliver, maxDeliver))
|
||||
}
|
||||
sort.Strings(why)
|
||||
return strings.Join(why, "; ")
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -19,7 +18,7 @@ type Watcher interface {
|
||||
// Followed are the states whose change composes again: placement, the indexer's manifests by module, the resolved
|
||||
// config values, the node agents' haves (a machine's first have, and the bus node's loaded list) and the assigner's
|
||||
// wants (the manifest each machine is wanted to run), and the vault's hashes of the bus accounts.
|
||||
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadLease}
|
||||
var Followed = []string{ReadAssignments, ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement}
|
||||
|
||||
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
|
||||
// open is said, composed without, and opened again at the next wake (any change it does hear, or a reconnect).
|
||||
@@ -29,9 +28,9 @@ type Follower struct {
|
||||
Logf func(string, ...any)
|
||||
// Asked watches the part's own ask to compose now (a tool's compose-now), waking at each; nil watches none.
|
||||
Asked func(wake func()) error
|
||||
// Hear hears the controller seat's condition events and the vault's account-issued (Consumes) and hands each, by event
|
||||
// name, with its body, to handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass:
|
||||
// a machine's root-free word changed; and so does an account the vault minted (ADR 0561 §1), whose hash the list carries.
|
||||
// Hear hears the controller seat's condition events (Consumes) and hands each, by event name, with its body, to
|
||||
// handle; nil hears none. A condition of the kind root-not-free raised or cleared wakes a pass: a machine's
|
||||
// root-free word changed.
|
||||
Hear func(handle func(event string, body []byte)) error
|
||||
|
||||
wake chan struct{}
|
||||
@@ -47,8 +46,6 @@ type Follower struct {
|
||||
held string
|
||||
notAsserted string
|
||||
refusedMans string
|
||||
// leaseOn is whether the lease was handed away at its last change heard, once leaseSeen.
|
||||
leaseOn, leaseSeen bool
|
||||
}
|
||||
|
||||
func (f *Follower) init() {
|
||||
@@ -88,15 +85,6 @@ func (f *Follower) logf(format string, args ...any) {
|
||||
// heard says whether a change composes again: every change but a have's, and of the haves a machine's first and the
|
||||
// bus module's loaded list moving.
|
||||
func (f *Follower) heard(state, key string, value []byte) bool {
|
||||
if state == ReadLease {
|
||||
// Renewed every few seconds by its holder: only the write that hands it away, or back, composes again (ADR 0561 §6).
|
||||
on, _ := buscompose.LeaseHandedAway(value, len(value) > 0, nil)
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
was, seen := f.leaseOn, f.leaseSeen
|
||||
f.leaseOn, f.leaseSeen = on, true
|
||||
return !seen || was != on
|
||||
}
|
||||
if state != ReadHave {
|
||||
return true
|
||||
}
|
||||
@@ -136,7 +124,7 @@ func (f *Follower) open() {
|
||||
}
|
||||
if f.Hear != nil && !f.heard_ {
|
||||
err := f.Hear(func(event string, body []byte) {
|
||||
if IsAccountIssued(event) || IsRootNotFree(body) {
|
||||
if IsRootNotFree(body) {
|
||||
f.Wake()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// **The grants the part writes** (novox/hq ADR 0547 §4, §5, ADR 0561 §2, §6; issue 774). A module's grant is the record
|
||||
// that its bus credential exists for the fingerprint the catalogue's trunk names for it: `<module>.<fingerprint hex>` in
|
||||
// the part's state grants, naming the accounts (the module's own and each process's) the vault holds a hash for. It is the
|
||||
// controller's grant as the controller composes it (cmd/mesh-controller grants.go grantsFrom), so the two compare by key.
|
||||
// Until the controller's lease is handed away nobody reads the part's keys: the assigner reads the controller's
|
||||
// (delivery.GrantSource), and the tool grants says every difference, so the switch is made on a measured equality.
|
||||
//
|
||||
// A module of which an account has no hash yet is granted the accounts that have one; a module none of whose accounts has
|
||||
// one has no grant, and one it was written for is deleted (an older grant naming a node whose account is gone would let the
|
||||
// assigner want it, and a grant naming no account names every node). Nothing else is ever deleted, and nothing is written
|
||||
// or deleted in a pass that could not read the vault's hashes, the indexer's modules or a manifest of the module.
|
||||
|
||||
// GrantsWritten is what one pass did to the state grants.
|
||||
type GrantsWritten struct {
|
||||
Wanted int `json:"wanted"`
|
||||
Written []string `json:"written,omitempty"`
|
||||
Deleted []string `json:"deleted,omitempty"`
|
||||
// Kept says why a pass wrote and deleted nothing, or left a module out.
|
||||
Kept []string `json:"kept,omitempty"`
|
||||
// Missing is each module the placement or an assignment puts on a machine that has no grant after the pass,
|
||||
// `<module>: <why>`: the assigner's sixth rule wants none of it (MissingGrants). Never passed over in silence: once
|
||||
// the part's grants are the ones read, it is the condition mesh-grants.grant-missing.
|
||||
Missing []string `json:"missing,omitempty"`
|
||||
}
|
||||
|
||||
// GrantMissingCondition is the key of the warning raised while a placed module has no grant (issue 1126).
|
||||
const GrantMissingCondition = "mesh-grants.grant-missing"
|
||||
|
||||
// MissingGrants is each module assigned or placed on a machine that want leaves without a grant, `<module>: <why>`,
|
||||
// sorted: its manifest refused (skip), no fingerprint of the new form at the indexer's trunk, or no account with a hash
|
||||
// in the vault (drop). **The fingerprint is the indexer's, never a commit's** (issue 1126: link2pay, snake, f1-circuits
|
||||
// and txt-game, whose source commit has not moved since a build of the old form, have a done key of the new form at it,
|
||||
// and that is the fingerprint granted). Pure.
|
||||
func MissingGrants(in Inputs, want map[string]delivery.ControllerGrant, drop []string, skip map[string]bool) []string {
|
||||
placed := map[string]bool{}
|
||||
for _, ms := range in.Assigned {
|
||||
for _, m := range ms {
|
||||
placed[m] = true
|
||||
}
|
||||
}
|
||||
var out []string
|
||||
for _, module := range sortedKeys(placed) {
|
||||
fp, has := in.Fingerprints[module]
|
||||
key, err := delivery.PartGrantKey(module, fp)
|
||||
if has && err == nil {
|
||||
if _, granted := want[key]; granted {
|
||||
continue
|
||||
}
|
||||
}
|
||||
why := ""
|
||||
switch {
|
||||
case skip[module]:
|
||||
why = "its manifest at the indexer's trunk is refused"
|
||||
case !has:
|
||||
why = "no fingerprint of the new form: " + orSaid(in.NoFingerprint[module], "the indexer holds no value of it")
|
||||
case err != nil:
|
||||
why = fmt.Sprintf("no grant key for %s: %v", fp, err)
|
||||
case slices.Contains(drop, key):
|
||||
why = "none of its accounts at " + fp + " has a hash in the vault"
|
||||
default:
|
||||
why = "no grant composed at " + fp
|
||||
}
|
||||
out = append(out, module+": "+why)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func orSaid(s, none string) string {
|
||||
if s == "" {
|
||||
return none
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// sayMissing raises mesh-grants.grant-missing while a placed module has no grant, once the controller's lease is handed
|
||||
// away (the part's grants are then the ones the assigner reads), and clears it when every one has one; in shadow the
|
||||
// pass and the tool say them. Said once per set, again when it changes, and always at the first pass after a start.
|
||||
func (p *Part) sayMissing(missing []string) {
|
||||
if p.Say == nil {
|
||||
return
|
||||
}
|
||||
why := ""
|
||||
if on, _ := leaseHandedAway(p.Read); on && len(missing) > 0 {
|
||||
why = strings.Join(missing, "; ")
|
||||
}
|
||||
p.holdMu.Lock()
|
||||
defer p.holdMu.Unlock()
|
||||
if p.missingKnown && why == p.missingSaid {
|
||||
return
|
||||
}
|
||||
c := asks.Condition{Key: GrantMissingCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
|
||||
if why == "" {
|
||||
c.State = asks.StateCleared
|
||||
} else {
|
||||
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
|
||||
c.Summary = fmt.Sprintf("%d placed module(s) have no grant, so no want can name them", len(missing))
|
||||
c.Headline, c.Needs = "A placed module has no bus grant", "a look at why each has none"
|
||||
c.Explanation = asks.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " +
|
||||
"credential rule gives them no want: " + why
|
||||
c.Resolved = "every placed module has a grant at the fingerprint the indexer names"
|
||||
}
|
||||
if p.Say(c) == nil {
|
||||
p.missingSaid, p.missingKnown = why, true
|
||||
}
|
||||
}
|
||||
|
||||
// GrantsOf is the grant of every module at the fingerprint the indexer's trunk names for it, by key, and the keys that
|
||||
// have none (drop): see the comment above. A module in skip (its manifest was refused) is neither. Pure.
|
||||
func GrantsOf(in Inputs, recs buscompose.Records, skip map[string]bool) (map[string]delivery.ControllerGrant, []string, error) {
|
||||
principals, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
accounts, missing := map[string][]string{}, map[string]bool{}
|
||||
for _, p := range principals {
|
||||
if p.Module == "" || (p.Kind != buscompose.KindModule && p.Kind != buscompose.KindProcess) {
|
||||
continue
|
||||
}
|
||||
if in.Hashes[p.Username()].Hash == "" {
|
||||
missing[p.Module] = true
|
||||
continue
|
||||
}
|
||||
accounts[p.Module] = append(accounts[p.Module], p.Username())
|
||||
}
|
||||
out := map[string]delivery.ControllerGrant{}
|
||||
var drop []string
|
||||
for module, fp := range in.Fingerprints {
|
||||
if skip[module] || !strings.HasPrefix(fp, delivery.SourcePrefix2) {
|
||||
continue
|
||||
}
|
||||
key, err := delivery.PartGrantKey(module, fp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
names := accounts[module]
|
||||
if missing[module] && len(names) == 0 {
|
||||
drop = append(drop, key)
|
||||
continue
|
||||
}
|
||||
sort.Strings(names)
|
||||
out[key] = delivery.ControllerGrant{Accounts: names}
|
||||
}
|
||||
sort.Strings(drop)
|
||||
return out, drop, nil
|
||||
}
|
||||
|
||||
// grantsReadFailed names a read of this pass without which no grant is written: the hashes and the indexer's modules and
|
||||
// manifests. A manifest refused for one module only leaves that module out (writeGrants).
|
||||
func grantsReadFailed(in Inputs) string {
|
||||
for _, g := range in.Gaps {
|
||||
if !g.Failed {
|
||||
continue
|
||||
}
|
||||
for _, input := range []string{ReadAccounts, ReadModules, ReadManifests} {
|
||||
if strings.HasPrefix(g.Input, input) {
|
||||
return g.Input + ": " + g.Why
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// writeGrants writes each grant whose key is absent or differs and says grant-written for each, and deletes the key of a
|
||||
// module that has none now.
|
||||
func (p *Part) writeGrants(res *Result, in Inputs, recs buscompose.Records) {
|
||||
if why := grantsReadFailed(in); why != "" {
|
||||
res.Grants = &GrantsWritten{Kept: []string{"no grant is written or deleted in a pass that could not read " + why}}
|
||||
return
|
||||
}
|
||||
skip := map[string]bool{}
|
||||
for _, g := range in.Refused {
|
||||
skip[strings.TrimPrefix(g.Input, "manifest of ")] = true
|
||||
}
|
||||
want, drop, err := GrantsOf(in, recs, skip)
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, "the grants could not be composed: "+err.Error())
|
||||
return
|
||||
}
|
||||
done := &GrantsWritten{Wanted: len(want), Missing: MissingGrants(in, want, drop, skip)}
|
||||
res.Grants = done
|
||||
p.sayMissing(done.Missing)
|
||||
for _, key := range sortedKeys(want) {
|
||||
g := want[key]
|
||||
data, err := delivery.EncodeControllerGrant(g)
|
||||
if err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("the grant %s cannot be encoded: %v", key, err))
|
||||
continue
|
||||
}
|
||||
if raw, _, found, err := p.Store.Get(StateGrants, key); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateGrants, key, err))
|
||||
continue
|
||||
} else if found {
|
||||
if was, err := delivery.DecodeControllerGrant(raw); err == nil && delivery.SameControllerGrant(was, withVersion(g)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, err := p.Store.Put(StateGrants, key, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateGrants, key, err))
|
||||
continue
|
||||
}
|
||||
done.Written = append(done.Written, key)
|
||||
module, fp, _ := delivery.ParsePartGrantKey(key)
|
||||
p.say(res, EventGrantWritten, map[string]any{"module": module, "fingerprint": fp, "accounts": g.Accounts})
|
||||
}
|
||||
for _, key := range drop {
|
||||
if _, _, found, err := p.Store.Get(StateGrants, key); err != nil || !found {
|
||||
continue
|
||||
}
|
||||
if err := p.Store.Delete(StateGrants, key); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be deleted: %v", StateGrants, key, err))
|
||||
continue
|
||||
}
|
||||
done.Deleted = append(done.Deleted, key)
|
||||
}
|
||||
}
|
||||
|
||||
func withVersion(g delivery.ControllerGrant) delivery.ControllerGrant {
|
||||
g.V = delivery.Version
|
||||
return g
|
||||
}
|
||||
|
||||
// GrantDifference is one key on which the part's grants and the controller's differ.
|
||||
type GrantDifference struct {
|
||||
Key string `json:"key"`
|
||||
// Kind is `only the controller's`, `only the part's` or `accounts differ`.
|
||||
Kind string `json:"kind"`
|
||||
Controller []string `json:"controller_accounts,omitempty"`
|
||||
Part []string `json:"part_accounts,omitempty"`
|
||||
}
|
||||
|
||||
// CompareGrants lists every key the two sets differ on, in key order: a key held by one alone, or held by both with other
|
||||
// accounts. Pure.
|
||||
func CompareGrants(controller, part map[string]delivery.ControllerGrant) []GrantDifference {
|
||||
keys := map[string]bool{}
|
||||
for k := range controller {
|
||||
keys[k] = true
|
||||
}
|
||||
for k := range part {
|
||||
keys[k] = true
|
||||
}
|
||||
out := []GrantDifference{}
|
||||
for _, k := range sortedKeys(keys) {
|
||||
c, inC := controller[k]
|
||||
g, inP := part[k]
|
||||
switch {
|
||||
case inC && !inP:
|
||||
out = append(out, GrantDifference{Key: k, Kind: "only the controller's", Controller: c.Accounts})
|
||||
case inP && !inC:
|
||||
out = append(out, GrantDifference{Key: k, Kind: "only the part's", Part: g.Accounts})
|
||||
case !delivery.SameControllerGrant(withVersion(c), withVersion(g)):
|
||||
out = append(out, GrantDifference{Key: k, Kind: "accounts differ", Controller: c.Accounts, Part: g.Accounts})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -38,11 +38,9 @@ const (
|
||||
ReadPlacement = delivery.PlacementRead
|
||||
)
|
||||
|
||||
// Reads is every state the part reads, in the manifest's order. ReadAssignments stays (and in Followed) only while the
|
||||
// controller holds the lease, when the assignments are unioned with the placement (ReadInputs); after the lease write
|
||||
// they place nothing, and the first change after it takes them out (../assignments-readers names the part until then).
|
||||
// Reads is every state the part reads, in the manifest's order.
|
||||
var Reads = []string{ReadAssignments, ReadControllerGrant, ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
|
||||
ReadAccounts, ReadPlacement, ReadLease}
|
||||
ReadAccounts, ReadPlacement}
|
||||
|
||||
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
|
||||
const (
|
||||
@@ -107,7 +105,7 @@ type resolved struct {
|
||||
type Inputs struct {
|
||||
// Nodes is every machine seen: in a node agent's have or a standing assignment.
|
||||
Nodes []string
|
||||
// Assigned is each machine's standing assignments and what the seat's placement puts there, sorted: while both
|
||||
// Assigned is each machine's standing assignments and what the catalogue's placement puts there, sorted: while both
|
||||
// stand (ADR 0549 §5) a module either names keeps its membership.
|
||||
Assigned map[string][]string
|
||||
// Placed is each node's placement as the seat mesh-placement holds it (ADR 0567 §1); empty before any is read.
|
||||
@@ -117,12 +115,6 @@ type Inputs struct {
|
||||
Manifests map[string]catalogue.Manifest
|
||||
// Digests is each module's manifest digest, by module.
|
||||
Digests map[string]string
|
||||
// Fingerprints is the fingerprint of the new form the indexer's trunk value names for each module, by module: what a
|
||||
// grant is written for (grants.go).
|
||||
Fingerprints map[string]string
|
||||
// NoFingerprint is why the indexer's value of a module names no fingerprint of the new form, by module (its none2, or
|
||||
// that it names none): a module here gets no grant, and that is said (grants.go MissingGrants).
|
||||
NoFingerprint map[string]string
|
||||
// Running is the manifest a machine is wanted to run, by `<node>.<module>`: the one the builder built at the
|
||||
// fingerprint the assigner's want names. An assignment with none is composed from the trunk's manifest.
|
||||
Running map[string]catalogue.Manifest
|
||||
@@ -146,22 +138,6 @@ type HeldHash struct {
|
||||
Generation int `json:"generation,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
// Fingerprint is the vault's fingerprint of the value the hash is of: with the generation, what says the password
|
||||
// changed, never the hash itself (a hash of the same password differs by its salt).
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
// Rotating is the vault's rotation of the account under way: a change of its hash it announces (ADR 0561 §4, §7).
|
||||
Rotating *struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"rotating,omitempty"`
|
||||
}
|
||||
|
||||
// credentialOf is what says which password a held hash is of, never the hash: adopted, or the vault's generation and the
|
||||
// fingerprint of its value.
|
||||
func credentialOf(h HeldHash) string {
|
||||
if h.Adopted {
|
||||
return "adopted"
|
||||
}
|
||||
return fmt.Sprintf("generation %d %s", h.Generation, h.Fingerprint)
|
||||
}
|
||||
|
||||
// readHashes reads the vault's bus-accounts; a refusal is a gap, and every user then has no hash held.
|
||||
@@ -245,23 +221,19 @@ func each(r Reader, state string, gaps *[]Gap) map[string][]byte {
|
||||
|
||||
// ReadInputs reads what the shadow composes from. It never fails: what it cannot read is a gap.
|
||||
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 := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{},
|
||||
Running: map[string]catalogue.Manifest{},
|
||||
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
|
||||
nodes := map[string]bool{}
|
||||
// **After the lease write the assignments place nothing** (novox/hq ADR 0549 §3, §6): they are frozen then, so a
|
||||
// module the catalogue takes off a node would keep its account, grant, membership and user-list entry. Once the lease
|
||||
// is handed away and the placement is read, the placement alone places modules, as the assigner's Placed does with
|
||||
// the same lease reading (buscompose.LeaseHandedAway); before, or with no placement on the bus, the two are unioned.
|
||||
byAssignment, busByAssignment := map[string][]string{}, ""
|
||||
for _, raw := range each(r, ReadAssignments, &in.Gaps) {
|
||||
var a assignment
|
||||
if json.Unmarshal(raw, &a) != nil || !a.stands() || a.Node == "" || a.Module == "" {
|
||||
continue
|
||||
}
|
||||
byAssignment[a.Node] = append(byAssignment[a.Node], a.Module)
|
||||
nodes[a.Node] = true
|
||||
in.Assigned[a.Node] = append(in.Assigned[a.Node], a.Module)
|
||||
if a.Module == BusModule {
|
||||
busByAssignment = a.Node
|
||||
in.BusNode = a.Node
|
||||
}
|
||||
}
|
||||
// The seat's placement (ADR 0567 §9): what it places is composed beside the assignments, and the seats held once
|
||||
@@ -285,27 +257,11 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
in.Placed = map[string]delivery.Placement{}
|
||||
}
|
||||
in.Placed[node] = p
|
||||
}
|
||||
}
|
||||
away, _ := leaseHandedAway(r)
|
||||
if away && len(in.Placed) > 0 {
|
||||
in.Gaps = append(in.Gaps, Gap{Input: ReadAssignments, Why: "not used: the controller's lease is handed away, " +
|
||||
"so the seat's placement alone places modules (ADR 0549 §3, §6)"})
|
||||
} else {
|
||||
for node, ms := range byAssignment {
|
||||
nodes[node] = true
|
||||
in.Assigned[node] = append(in.Assigned[node], ms...)
|
||||
}
|
||||
in.BusNode = busByAssignment
|
||||
}
|
||||
for node, p := range in.Placed {
|
||||
nodes[node] = true
|
||||
for _, m := range p.Assigned() {
|
||||
if !slices.Contains(in.Assigned[node], m) {
|
||||
in.Assigned[node] = append(in.Assigned[node], m)
|
||||
}
|
||||
if m == BusModule && in.BusNode == "" {
|
||||
in.BusNode = node
|
||||
for _, m := range p.Assigned() {
|
||||
if !slices.Contains(in.Assigned[node], m) {
|
||||
in.Assigned[node] = append(in.Assigned[node], m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,15 +322,6 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
if err == nil && v.Manifest == "" {
|
||||
err = fmt.Errorf("the indexer's value names no manifest digest")
|
||||
}
|
||||
if err == nil && strings.HasPrefix(v.FP2, delivery.SourcePrefix2) {
|
||||
in.Fingerprints[module] = v.FP2
|
||||
} else if err == nil {
|
||||
why := v.None2
|
||||
if why == "" {
|
||||
why = "the indexer's value names no fingerprint of the new form"
|
||||
}
|
||||
in.NoFingerprint[module] = why
|
||||
}
|
||||
var m catalogue.Manifest
|
||||
if err == nil {
|
||||
m, err = cache.get(r, v.Manifest)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||
)
|
||||
@@ -24,38 +23,31 @@ const (
|
||||
StateMemberships = "memberships"
|
||||
StateAssertions = "assertions"
|
||||
StateTakeover = "takeover"
|
||||
// StateGrants is the part's grants by module and fingerprint, the key layout of the controller's (grants.go).
|
||||
StateGrants = delivery.PartGrantsState
|
||||
KeyList = "list"
|
||||
KeySwitch = "switch"
|
||||
KeyList = "list"
|
||||
KeySwitch = "switch"
|
||||
// KeyPass is the last pass's result, and KeyAsked a tool's ask to compose again now, both in StateUserList.
|
||||
KeyPass = "pass"
|
||||
KeyAsked = "asked"
|
||||
)
|
||||
|
||||
// States is every state the part keeps.
|
||||
var States = []string{StateAccounts, StateAssertions, StateGrants, StateMemberships, StateTakeover, StateUserList}
|
||||
var States = []string{StateAssertions, StateMemberships, StateTakeover, StateUserList}
|
||||
|
||||
// The events the part says.
|
||||
const (
|
||||
EventUserList = "user-list-changed"
|
||||
EventMembership = "membership-changed"
|
||||
// EventGrantWritten is said when the part wrote a grant of its state grants (ADR 0547 §5, ADR 0561 §2).
|
||||
EventGrantWritten = "grant-written"
|
||||
// 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{EventAsserted, EventGrantWritten, EventHashesAccepted, EventMembership, EventUserList}
|
||||
var Emits = []string{EventAsserted, EventMembership, EventUserList}
|
||||
|
||||
// Store is the part's own state, by local name: the only thing the part writes.
|
||||
type Store interface {
|
||||
Get(state, key string) ([]byte, uint64, bool, error)
|
||||
Put(state, key string, value []byte) (uint64, error)
|
||||
// Keys and Delete are for the wanted accounts (accounts.go) and the grants (grants.go): every other state is only put.
|
||||
Keys(state string) ([]string, error)
|
||||
Delete(state, key string) error
|
||||
}
|
||||
|
||||
// Emitter says one of the part's events.
|
||||
@@ -119,15 +111,9 @@ type Result struct {
|
||||
// free, <why>`: a verified sender is composed as from no root-free machine there (issue 1945).
|
||||
RootNotFree []string `json:"root_not_free,omitempty"`
|
||||
// Sealed is the list sealed to the bus node as the key `user-list.sealed` names it (ADR 0547 clause 7).
|
||||
Sealed *SealedState `json:"sealed,omitempty"`
|
||||
// Consumers is what the pass found and created of the durable consumers an account needs (ADR 0561 §6).
|
||||
Consumers *ConsumersState `json:"consumers,omitempty"`
|
||||
// Accounts is what the pass wrote of the wanted accounts the vault mints from (ADR 0561 §2).
|
||||
Accounts *AccountsWritten `json:"accounts,omitempty"`
|
||||
// Grants is what the pass wrote of its grants (grants.go).
|
||||
Grants *GrantsWritten `json:"grants,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
PublishedOrAsserted string `json:"published_or_asserted"`
|
||||
Sealed *SealedState `json:"sealed,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.
|
||||
@@ -150,12 +136,6 @@ type Part struct {
|
||||
Logf func(string, ...any)
|
||||
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
|
||||
Objects Objects
|
||||
// Consumers creates an account's durable consumer from the lease write (consumers.go): the process's connection;
|
||||
// nil in the tools, which compare only.
|
||||
Consumers ConsumerCreator
|
||||
// Bus is where the nodes reach the bus, which each wanted account carries (accounts.go): the process's own credential's;
|
||||
// empty in the tools, which write no account.
|
||||
Bus BusAddress
|
||||
// Say says a condition to the operator: the process's; nil in the tools.
|
||||
Say func(asks.Condition) error
|
||||
// Forget takes away the record the asker kept of an ask it failed to publish; Nonce is a test's id suffix.
|
||||
@@ -177,14 +157,6 @@ type Part struct {
|
||||
holdMu sync.Mutex
|
||||
heldSaid string
|
||||
heldKnown bool
|
||||
// differSaid is the account consumers' differences last said as a condition, once differKnown (consumers.go).
|
||||
differSaid string
|
||||
differKnown bool
|
||||
// missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go).
|
||||
missingSaid string
|
||||
missingKnown bool
|
||||
// hashHold are the users whose hash changes held the pass's list, or nil (sealed.go).
|
||||
hashHold []string
|
||||
}
|
||||
|
||||
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
|
||||
@@ -193,21 +165,18 @@ type Ask struct {
|
||||
By string `json:"by"`
|
||||
At time.Time `json:"at"`
|
||||
Assert string `json:"assert,omitempty"`
|
||||
// Consumer is, from grants_consumer_match, one account consumer `<stream>.<name>` to bring to the composed
|
||||
// configuration by hand (ADR 0561 §6).
|
||||
Consumer string `json:"consumer,omitempty"`
|
||||
}
|
||||
|
||||
// asked is a new ask, read once per ask; the zero Ask when none.
|
||||
func (p *Part) asked() Ask {
|
||||
// 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 Ask{}
|
||||
return ""
|
||||
}
|
||||
p.askSeen = rev
|
||||
var a Ask
|
||||
_ = json.Unmarshal(raw, &a)
|
||||
return a
|
||||
return a.Assert
|
||||
}
|
||||
|
||||
func (p *Part) now() time.Time {
|
||||
@@ -271,14 +240,6 @@ func (p *Part) Compose() Result {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The accounts the vault mints from, said in the state only it reads (ADR 0561 §2).
|
||||
if p.Store != nil {
|
||||
p.writeAccounts(&res, in, recs)
|
||||
}
|
||||
// The grants the assigner reads once the lease is handed away, in shadow until then (ADR 0547 §4, §5).
|
||||
if p.Store != nil {
|
||||
p.writeGrants(&res, in, recs)
|
||||
}
|
||||
// The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7).
|
||||
if list, n, leftOut, err := ComposeList(recs, in.Hashes); err != nil {
|
||||
res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error())
|
||||
@@ -313,11 +274,10 @@ func (p *Part) Compose() Result {
|
||||
}
|
||||
}
|
||||
held, heldErr := HeldStreams(p.Call)
|
||||
var ask Ask
|
||||
if p.Create != nil || p.Consumers != nil {
|
||||
ask = p.asked()
|
||||
forced := ""
|
||||
if p.Create != nil {
|
||||
forced = p.askedAssert()
|
||||
}
|
||||
forced := ask.Assert
|
||||
for _, module := range sortedKeys(in.Manifests) {
|
||||
if !assigned(in, module) {
|
||||
continue
|
||||
@@ -339,7 +299,6 @@ func (p *Part) Compose() Result {
|
||||
res.AssertionsWritten++
|
||||
}
|
||||
}
|
||||
p.consumers(&res, recs, ask.Consumer)
|
||||
if _, _, found, err := p.Store.Get(StateTakeover, KeySwitch); err == nil && !found {
|
||||
data, _ := json.Marshal(Switch{V: 1, State: Shadow, Acts: []Act{}})
|
||||
if _, err := p.Store.Put(StateTakeover, KeySwitch, data); err != nil {
|
||||
@@ -365,11 +324,6 @@ func (p *Part) Last() Result {
|
||||
}
|
||||
|
||||
func assigned(in Inputs, module string) bool {
|
||||
// The node-engine runs on every node though no node is assigned it (novox/hq ADR 0563 §2, §3): its manifest is
|
||||
// placed everywhere, so its declared state is asserted and created as a placed module's.
|
||||
if module == buscompose.EngineModule {
|
||||
return true
|
||||
}
|
||||
for _, ms := range in.Assigned {
|
||||
for _, m := range ms {
|
||||
if m == module {
|
||||
|
||||
@@ -78,11 +78,6 @@ func Records(in Inputs) (buscompose.Records, []Gap) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
|
||||
// part; it adds no principal, since each node's engine speaks as the node's account.
|
||||
if m, ok := in.Manifests[buscompose.EngineModule]; ok {
|
||||
out.Engine = &buscompose.Declared{Module: m.Module, State: bucketsOf(m), Logs: logsOf(m), Streams: streamsOf(m)}
|
||||
}
|
||||
return out, gaps
|
||||
}
|
||||
|
||||
|
||||
@@ -23,17 +23,8 @@ const (
|
||||
KindRootNotFree = "root-not-free"
|
||||
)
|
||||
|
||||
// EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's
|
||||
// bus-accounts, so a pass composes the list with it.
|
||||
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
|
||||
|
||||
// Consumes is every event the part hears, as its manifest's consumes name them.
|
||||
var Consumes = []string{EventConditionRaised, EventConditionCleared, EventAccountIssued}
|
||||
|
||||
// IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject.
|
||||
func IsAccountIssued(event string) bool {
|
||||
return event == EventAccountIssued || event == buscompose.AccountIssuedSubject
|
||||
}
|
||||
var Consumes = []string{EventConditionRaised, EventConditionCleared}
|
||||
|
||||
// EventSubject is the bus subject of a controller seat event named `mesh-controller.<event>`.
|
||||
func EventSubject(event string) string {
|
||||
|
||||
@@ -2,12 +2,10 @@ package grants
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
@@ -93,12 +91,11 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
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 next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus, lost, digest) != "":
|
||||
case next.Follow == delivery.FollowGrants && cur.Digest != digest && p.holdsBack(in, leftOut, bus, lost) != "":
|
||||
// After take-over 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, bus, lost, digest)
|
||||
why = p.holdsBack(in, leftOut, bus, lost)
|
||||
held = why
|
||||
p.keepHashHold(res, digest)
|
||||
case cur.Digest != digest || cur.SealedTo != delivery.SealingKeyID(key) || cur.Node != in.BusNode:
|
||||
object, err := delivery.SealUserList(list, key)
|
||||
if err == nil {
|
||||
@@ -136,8 +133,7 @@ func (p *Part) writeSealed(res *Result, list []byte, users int, leftOut []string
|
||||
return
|
||||
}
|
||||
if next.Digest == digest && cur.Digest != digest {
|
||||
p.keepWrittenUsers(res, list, in.Hashes)
|
||||
p.acceptedWritten(res, digest)
|
||||
p.keepWrittenUsers(res, list)
|
||||
}
|
||||
// The object the key named before is taken away once the key names the new one: the bucket keeps the list in force.
|
||||
if had && cur.Object != "" && cur.Object != next.Object {
|
||||
@@ -292,50 +288,11 @@ func (p *Part) followReasons(res Result, in Inputs) []string {
|
||||
// composed after take-over must keep.
|
||||
const KeyWrittenUsers = "sealed-users"
|
||||
|
||||
// KeyWrittenCredentials is the key of the state user-list naming, per user of the list in force, which password its hash
|
||||
// is of (credentialOf: adopted, or the vault's generation and fingerprint), never the hash: what tells a list that changes
|
||||
// several loaded users' hashes at once (ADR 0561 §7).
|
||||
const KeyWrittenCredentials = "sealed-credentials"
|
||||
|
||||
func (p *Part) keepWrittenUsers(res *Result, list []byte, hashes map[string]HeldHash) {
|
||||
users := listUsers(list)
|
||||
data, _ := json.Marshal(users)
|
||||
func (p *Part) keepWrittenUsers(res *Result, list []byte) {
|
||||
data, _ := json.Marshal(listUsers(list))
|
||||
if _, err := p.Store.Put(StateUserList, KeyWrittenUsers, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenUsers, err))
|
||||
}
|
||||
creds := map[string]string{}
|
||||
for _, u := range users {
|
||||
if h, ok := hashes[u]; ok {
|
||||
creds[u] = credentialOf(h)
|
||||
}
|
||||
}
|
||||
data, _ = json.Marshal(creds)
|
||||
if _, err := p.Store.Put(StateUserList, KeyWrittenCredentials, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyWrittenCredentials, err))
|
||||
}
|
||||
}
|
||||
|
||||
// changedHashes are the users of the list in force whose hash the list composed now would change with no rotation by the
|
||||
// vault naming them, sorted; none when the part kept no credentials of the list in force (a list written before).
|
||||
func (p *Part) changedHashes(hashes map[string]HeldHash) ([]string, error) {
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyWrittenCredentials)
|
||||
if err != nil || !found {
|
||||
return nil, err
|
||||
}
|
||||
var had map[string]string
|
||||
if err := json.Unmarshal(raw, &had); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var changed []string
|
||||
for user, was := range had {
|
||||
h, ok := hashes[user]
|
||||
if !ok || credentialOf(h) == was || h.Rotating != nil {
|
||||
continue
|
||||
}
|
||||
changed = append(changed, user)
|
||||
}
|
||||
slices.Sort(changed)
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
var listUser = regexp.MustCompile(`\{\s*user:\s*"([^"]+)"`)
|
||||
@@ -355,8 +312,7 @@ func listUsers(list []byte) []string {
|
||||
// loaded list not read holds it too, since that cannot be ruled out). The list in force is the one the part wrote, by the users
|
||||
// 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.
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string, digest string) string {
|
||||
p.hashHold = nil
|
||||
func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string) string {
|
||||
if f := in.ReadFailures(); len(f) > 0 {
|
||||
return "held: the list in force is kept while a read of this pass failed (" + strings.Join(f, "; ") + ")"
|
||||
}
|
||||
@@ -386,127 +342,5 @@ func (p *Part) holdsBack(in Inputs, leftOut, bus, lost []string, digest string)
|
||||
if len(unhashed) > 0 {
|
||||
return "held: the list in force is kept, since " + strings.Join(unhashed, ", ") + " would be left out for want of a hash in the vault"
|
||||
}
|
||||
// More than one loaded user's hash changing at once, with no rotation by the vault naming each (ADR 0561 §7): a new
|
||||
// writer of hashes must not lock several holders out in one list, as nine seats were cut on 2026-10-27 at 09:21.
|
||||
changed, err := p.changedHashes(in.Hashes)
|
||||
if err != nil {
|
||||
return "held: the credentials of the list in force cannot be read: " + err.Error()
|
||||
}
|
||||
if len(changed) > 1 && !p.accepted(digest, changed) {
|
||||
p.hashHold = changed
|
||||
return "held: the list in force is kept, since the hashes of " + fmt.Sprint(len(changed)) + " loaded users would change at " +
|
||||
"once with no rotation by the vault naming them: " + strings.Join(changed, ", ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// **A person accepts a held list's hash changes** (ADR 0561 §7, §9; tools-are-how-a-module-is-spoken-to): the hold of
|
||||
// several loaded users' hashes changing at once is released only by grants_accept_hashes, naming exactly the users held,
|
||||
// with why, bound to the held list's digest. The acceptance releases this guard alone, once: the other holds (a failed
|
||||
// read, a user dropped for want of a hash, a lost right) stand whatever it says; and a list composed otherwise since is
|
||||
// held again, for a new acceptance.
|
||||
|
||||
// KeyHashHold names the list held for its hash changes (its digest and the users), and KeyHashAccept a person's acceptance.
|
||||
const (
|
||||
KeyHashHold = "hash-hold"
|
||||
KeyHashAccept = "hash-accept"
|
||||
)
|
||||
|
||||
// EventHashesAccepted is said when a list held for its hash changes is written on a person's acceptance: a hand act.
|
||||
const EventHashesAccepted = "hashes-accepted"
|
||||
|
||||
// HashHold is the value of KeyHashHold, and HashAccept of KeyHashAccept.
|
||||
type HashHold struct {
|
||||
Digest string `json:"digest"`
|
||||
Users []string `json:"users"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
type HashAccept struct {
|
||||
Digest string `json:"digest"`
|
||||
Users []string `json:"users"`
|
||||
By string `json:"by"`
|
||||
Why string `json:"why"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// keepHashHold writes which list is held for its hash changes, when that is the hold.
|
||||
func (p *Part) keepHashHold(res *Result, digest string) {
|
||||
if p.hashHold == nil {
|
||||
return
|
||||
}
|
||||
data, _ := json.Marshal(HashHold{Digest: digest, Users: p.hashHold, At: p.now()})
|
||||
if raw, _, found, err := p.Store.Get(StateUserList, KeyHashHold); err == nil && found {
|
||||
var was HashHold
|
||||
if json.Unmarshal(raw, &was) == nil && was.Digest == digest && slices.Equal(was.Users, p.hashHold) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := p.Store.Put(StateUserList, KeyHashHold, data); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyHashHold, err))
|
||||
}
|
||||
}
|
||||
|
||||
// accepted says a person accepted exactly these users' hash changes of the list of this digest.
|
||||
func (p *Part) accepted(digest string, users []string) bool {
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashAccept)
|
||||
if err != nil || !found {
|
||||
return false
|
||||
}
|
||||
var a HashAccept
|
||||
return json.Unmarshal(raw, &a) == nil && a.Digest == digest && slices.Equal(a.Users, users)
|
||||
}
|
||||
|
||||
// acceptedWritten says, once, that the list a person accepted is written, and forgets the acceptance and the hold.
|
||||
func (p *Part) acceptedWritten(res *Result, digest string) {
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashAccept)
|
||||
if err != nil || !found {
|
||||
return
|
||||
}
|
||||
var a HashAccept
|
||||
if json.Unmarshal(raw, &a) != nil || a.Digest != digest {
|
||||
return
|
||||
}
|
||||
p.say(res, EventHashesAccepted, map[string]any{"digest": digest, "users": a.Users, "by": a.By, "why": a.Why})
|
||||
for _, k := range []string{KeyHashAccept, KeyHashHold} {
|
||||
if err := p.Store.Delete(StateUserList, k); err != nil {
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be taken away: %v", StateUserList, k, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AcceptHashes is the tool accept-hashes: a person accepts the hash changes of the list held for them, naming exactly its
|
||||
// users, with why. Refused when no list is held for its hashes, or for another set of users. Bound to the held list's
|
||||
// digest: a list composed otherwise since is held again.
|
||||
func (p *Part) AcceptHashes(caller string, users []string, why string) (any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("accept-hashes says why (`why`)")
|
||||
}
|
||||
raw, _, found, err := p.Store.Get(StateUserList, KeyHashHold)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the held list cannot be read: %w", err)
|
||||
}
|
||||
var h HashHold
|
||||
if !found || json.Unmarshal(raw, &h) != nil || h.Digest == "" {
|
||||
return nil, errors.New("no list is held for its users' hashes changing: there is nothing to accept")
|
||||
}
|
||||
named := slices.Sorted(slices.Values(users))
|
||||
if !slices.Equal(named, h.Users) {
|
||||
return nil, fmt.Errorf("the list held (%s) changes the hashes of %s: an acceptance names exactly those, and %s is not that",
|
||||
short(h.Digest), strings.Join(h.Users, ", "), strings.Join(named, ", "))
|
||||
}
|
||||
data, _ := json.Marshal(HashAccept{Digest: h.Digest, Users: h.Users, By: someoneOr(caller), Why: strings.TrimSpace(why), At: p.now()})
|
||||
rev, err := p.Store.Put(StateUserList, KeyHashAccept, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the acceptance could not be written: %w", err)
|
||||
}
|
||||
return map[string]any{"accepted": rev, "digest": h.Digest, "users": h.Users, "says": "the part's process writes the held " +
|
||||
"list at its next pass, once, unless another hold stands or the list composed then is another; it says hashes-accepted"}, nil
|
||||
}
|
||||
|
||||
func someoneOr(caller string) string {
|
||||
if caller == "" {
|
||||
return "a caller the runtime did not prove"
|
||||
}
|
||||
return caller
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -20,92 +21,43 @@ const (
|
||||
ToolTakeOver = "grants_take_over"
|
||||
ToolTakeOverState = "grants_take_over_state"
|
||||
ToolHandBack = "grants_hand_back"
|
||||
ToolAccounts = "grants_accounts"
|
||||
ToolConsumerMatch = "grants_consumer_match"
|
||||
ToolAcceptHashes = "grants_accept_hashes"
|
||||
)
|
||||
|
||||
// Tools is every tool, in the manifest's order.
|
||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState,
|
||||
ToolAccounts, ToolConsumerMatch, ToolAcceptHashes}
|
||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolTakeOver, ToolHandBack, ToolTakeOverState}
|
||||
|
||||
// WritesGrants is what grants says of the part's own grants in this milestone: written, and read by nobody until the
|
||||
// controller's lease is handed away (delivery.GrantSource).
|
||||
const WritesGrants = "in shadow: the grants part writes grants.<module>.<fp> into its state grants, which the assigner reads only once the controller's lease is handed away (ADR 0547 clause 5); the controller's are beside, with every difference"
|
||||
// WritesNoGrant is what grants says of the part's own grants in this milestone.
|
||||
const WritesNoGrant = "none: the grants part writes no grants.<module>.<fp> before ADR 0547's third milestone (clause 5); the controller's are beside"
|
||||
|
||||
// Grants answers the tool grants: the grants the part wrote, the controller's, and every difference between them, one
|
||||
// module's or all.
|
||||
// Grants answers the tool grants: what the part writes of grants (none), and the controller's, one module's or all.
|
||||
func (p *Part) Grants(module string) (any, error) {
|
||||
out := map[string]any{"writes": WritesGrants}
|
||||
read := func(get func() ([]string, func(string) ([]byte, bool, error), error)) (map[string]delivery.ControllerGrant, error) {
|
||||
keys, one, err := get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]delivery.ControllerGrant{}
|
||||
for _, k := range keys {
|
||||
mod, _, err := delivery.ParseControllerGrantKey(k)
|
||||
if err != nil || (module != "" && mod != module) {
|
||||
continue
|
||||
}
|
||||
raw, found, err := one(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
g, err := delivery.DecodeControllerGrant(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", k, err)
|
||||
}
|
||||
m[k] = g
|
||||
}
|
||||
return m, nil
|
||||
keys, err := p.Read.Keys(ReadControllerGrant)
|
||||
if err != nil {
|
||||
return map[string]any{"writes": WritesNoGrant, "controller_unread": err.Error()}, nil
|
||||
}
|
||||
controller, cerr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
|
||||
keys, err := p.Read.Keys(ReadControllerGrant)
|
||||
return keys, func(k string) ([]byte, bool, error) { return p.Read.Get(ReadControllerGrant, k) }, err
|
||||
})
|
||||
mine, perr := read(func() ([]string, func(string) ([]byte, bool, error), error) {
|
||||
keys, err := p.Store.Keys(StateGrants)
|
||||
return keys, func(k string) ([]byte, bool, error) {
|
||||
raw, _, found, err := p.Store.Get(StateGrants, k)
|
||||
return raw, found, err
|
||||
}, err
|
||||
})
|
||||
type grant struct {
|
||||
Key string `json:"key"`
|
||||
Module string `json:"module"`
|
||||
Accounts []string `json:"accounts,omitempty"`
|
||||
Module string `json:"module"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Accounts []string `json:"accounts,omitempty"`
|
||||
}
|
||||
list := func(m map[string]delivery.ControllerGrant) []grant {
|
||||
out := []grant{}
|
||||
for _, k := range sortedKeys(m) {
|
||||
mod, _, _ := delivery.ParseControllerGrantKey(k)
|
||||
out = append(out, grant{Key: k, Module: mod, Accounts: m[k].Accounts})
|
||||
out := []grant{}
|
||||
for _, k := range keys {
|
||||
m, fp, err := delivery.ParseControllerGrantKey(k)
|
||||
if err != nil || (module != "" && m != module) {
|
||||
continue
|
||||
}
|
||||
return out
|
||||
g := grant{Module: m, Fingerprint: fp}
|
||||
if module != "" {
|
||||
if raw, found, err := p.Read.Get(ReadControllerGrant, k); err == nil && found {
|
||||
if v, err := delivery.DecodeControllerGrant(raw); err == nil {
|
||||
g.Accounts = v.Accounts
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, g)
|
||||
}
|
||||
if cerr != nil {
|
||||
out["controller_unread"] = cerr.Error()
|
||||
} else {
|
||||
out["controller"], out["controller_count"] = list(controller), len(controller)
|
||||
}
|
||||
if perr != nil {
|
||||
out["written_unread"] = perr.Error()
|
||||
} else {
|
||||
out["written"], out["count"] = list(mine), len(mine)
|
||||
}
|
||||
if cerr == nil && perr == nil {
|
||||
diff := CompareGrants(controller, mine)
|
||||
out["differences"], out["differing"] = diff, len(diff)
|
||||
out["equal"] = len(diff) == 0
|
||||
}
|
||||
if last := p.Last(); last.Grants != nil {
|
||||
out["last_pass"] = last.Grants
|
||||
}
|
||||
return out, nil
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Module+out[i].Fingerprint < out[j].Module+out[j].Fingerprint })
|
||||
return map[string]any{"writes": WritesNoGrant, "controller": out, "count": len(out)}, nil
|
||||
}
|
||||
|
||||
// MembershipOf answers the tool membership: one module on one node as composed now, and as last published by the
|
||||
|
||||
@@ -12,13 +12,11 @@
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"node-engine.have",
|
||||
"mesh-vault.bus-accounts",
|
||||
"mesh-controller.lease"
|
||||
"mesh-vault.bus-accounts"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-controller.condition-raised",
|
||||
"mesh-controller.condition-cleared",
|
||||
"mesh-vault.account-issued"
|
||||
"mesh-controller.condition-cleared"
|
||||
],
|
||||
"uses": [
|
||||
"operator-channel"
|
||||
@@ -31,8 +29,6 @@
|
||||
],
|
||||
"emits": [
|
||||
"asserted",
|
||||
"grant-written",
|
||||
"hashes-accepted",
|
||||
"membership-changed",
|
||||
"user-list-changed"
|
||||
],
|
||||
@@ -44,24 +40,13 @@
|
||||
"grants_assert",
|
||||
"grants_take_over",
|
||||
"grants_take_over_state",
|
||||
"grants_hand_back",
|
||||
"grants_accounts",
|
||||
"grants_consumer_match",
|
||||
"grants_accept_hashes"
|
||||
"grants_hand_back"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
"name": "accounts",
|
||||
"history": 5
|
||||
},
|
||||
{
|
||||
"name": "assertions",
|
||||
"history": 5
|
||||
},
|
||||
{
|
||||
"name": "grants",
|
||||
"history": 5
|
||||
},
|
||||
{
|
||||
"name": "memberships",
|
||||
"history": 5
|
||||
|
||||
Reference in New Issue
Block a user