The operator asked to remove the controller by finishing its hand-over; no tool said who holds the lease, which manifests still read, invoke or consume mesh-controller, or whether the lease write's check (ADR 0571 §13) would pass. grants_lease_check answers all three from bus state on the grants part, which already reads the lease, every manifest at the indexer's trunk and the bus's streams: the holder and what follows from it, every dependency with the record on the named keep list that keeps it, the controller's streams and buckets someone consumes, and the check run dry at the write (the three kept to it alone) and before its delivery, with reasons. Quick fix: the judgement and the keep list in the grants part, as here. Thorough fix: the keep list and the dependency derivation in mesh-sdk, shared by this tool and the §13 manifest tests in mesh-catalog, mesh-delivery and mesh-tools. Chosen quick: the tests do not exist yet and an sdk move needs a group; the move is named for the lead.
331 lines
12 KiB
Go
331 lines
12 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
|
)
|
|
|
|
// The part's tools, as its manifest lists them. Module tools in this milestone: a `mesh-*` seat must be in mesh-sdk's own
|
|
// seat list, which the running controller's composer does not know, so the part claims no seat until take-over.
|
|
const (
|
|
ToolGrants = "grants_grants"
|
|
ToolMembership = "grants_membership"
|
|
ToolUserList = "grants_user_list"
|
|
ToolComposeNow = "grants_compose_now"
|
|
ToolAssert = "grants_assert"
|
|
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, ToolLeaseCheck}
|
|
|
|
// 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"
|
|
|
|
// Grants answers the tool grants: the grants the part wrote, the controller's, and every difference between them, 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
|
|
}
|
|
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"`
|
|
}
|
|
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})
|
|
}
|
|
return out
|
|
}
|
|
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
|
|
}
|
|
|
|
// MembershipOf answers the tool membership: one module on one node as composed now, and as last published by the
|
|
// part's process (its state memberships says what became of each at the last pass).
|
|
func (p *Part) MembershipOf(node, module string) (any, error) {
|
|
if node == "" && module == "" {
|
|
return p.Held()
|
|
}
|
|
if node == "" || module == "" {
|
|
return nil, errors.New("membership names a node and a module, or neither for every membership held back")
|
|
}
|
|
in := ReadInputs(p.Read, &p.cache)
|
|
recs, left := Records(in)
|
|
notFree := WithRootFree(&recs, p.Call)
|
|
m, ok := Memberships(recs)[node+"."+module]
|
|
out := map[string]any{"node": node, "module": module, "gaps": MembershipGaps, "root_free": recs.RootFree, "root_not_free": notFree}
|
|
if ok {
|
|
out["composed"] = m
|
|
} else {
|
|
why := fmt.Sprintf("%s is not assigned to %s on the bus", module, node)
|
|
for _, g := range left {
|
|
if g.Input == module+" on "+node {
|
|
why = g.Why
|
|
}
|
|
}
|
|
out["composed"] = why
|
|
}
|
|
// The state holds the bare membership last published (mesh-sdk's codec, issue 774); what became of it at the last
|
|
// pass is the pass's, kept in user-list at KeyPass.
|
|
key := node + "." + module
|
|
if raw, rev, found, err := p.Store.Get(StateMemberships, key); err != nil {
|
|
out["published"] = "not readable: " + err.Error()
|
|
} else if !found {
|
|
out["published"] = "no pass of the part's process has published it yet"
|
|
} else if was, err := membership.DecodeGrantsMembership(raw); err != nil {
|
|
out["published"] = fmt.Sprintf("revision %d is not readable: %v", rev, err)
|
|
} else {
|
|
out["published"] = map[string]any{"revision": rev, "digest": digestOf(was), "membership": was}
|
|
}
|
|
out["last_pass"] = p.passOf(key)
|
|
return out, nil
|
|
}
|
|
|
|
// passOf is what the last pass the part's process kept says of one membership: published, unchanged or why not, and
|
|
// the seat verbs it dropped as retired.
|
|
func (p *Part) passOf(key string) any {
|
|
raw, rev, found, err := p.Store.Get(StateUserList, KeyPass)
|
|
if err != nil {
|
|
return "not readable: " + err.Error()
|
|
}
|
|
if !found {
|
|
return "no pass of the part's process has been kept yet"
|
|
}
|
|
var res Result
|
|
if err := json.Unmarshal(raw, &res); err != nil {
|
|
return "not readable: " + err.Error()
|
|
}
|
|
said := MembershipUnchanged
|
|
if slices.Contains(res.Published, key) {
|
|
said = MembershipPublished
|
|
}
|
|
for _, n := range res.NotPublished {
|
|
if k, why, _ := strings.Cut(n, ": "); k == key {
|
|
said = why
|
|
}
|
|
}
|
|
out := map[string]any{"revision": rev, "published": said}
|
|
if r := res.Retired[key]; len(r) > 0 {
|
|
out["retired"] = r
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Held answers every membership the part's process held back at its last pass, each `<node>.<module>` with why (what
|
|
// publishing would lose), from the pass it keeps (issue 1945).
|
|
func (p *Part) Held() (any, error) {
|
|
raw, rev, found, err := p.Store.Get(StateUserList, KeyPass)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the last pass cannot be read: %w", err)
|
|
}
|
|
if !found {
|
|
return map[string]any{"held": []any{}, "says": "no pass of the part's process has been kept yet"}, nil
|
|
}
|
|
var res Result
|
|
if err := json.Unmarshal(raw, &res); err != nil {
|
|
return nil, fmt.Errorf("the last pass cannot be read: %w", err)
|
|
}
|
|
type held struct {
|
|
Membership string `json:"membership"`
|
|
Why string `json:"why"`
|
|
}
|
|
out := []held{}
|
|
for _, n := range res.NotPublished {
|
|
key, why, _ := strings.Cut(n, ": ")
|
|
out = append(out, held{Membership: key, Why: why})
|
|
}
|
|
refused := res.Refused
|
|
if refused == nil {
|
|
refused = []string{}
|
|
}
|
|
return map[string]any{"held": out, "count": len(out), "pass": rev, "published": res.Published, "refused": refused}, nil
|
|
}
|
|
|
|
// Pass composes and keeps the result as the last pass, for the tools to read.
|
|
func (p *Part) Pass() Result {
|
|
res := p.Compose()
|
|
data, _ := json.Marshal(res)
|
|
if _, err := p.Store.Put(StateUserList, KeyPass, data); err != nil {
|
|
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateUserList, KeyPass, err))
|
|
}
|
|
return res
|
|
}
|
|
|
|
// NoGrantAgain is what compose-now says of the controller's grants: the controller serves no verb that writes a module's
|
|
// grant again (its issue replaces the account, which is not that), so a grant write it lost stays lost until it composes
|
|
// again; the part writes grants only from ADR 0547's third milestone.
|
|
const NoGrantAgain = "missing: a verb to ask the controller to write a module's grant again (mesh-controller serves none; its issue replaces the bus account, which is not that), so compose-now cannot ask it"
|
|
|
|
// AskCompose is the tool compose-now: it asks the part's process to compose and publish now (only the process holds the
|
|
// grant to publish a membership), and answers the last pass, whose revision moves when the process answered.
|
|
func (p *Part) AskCompose(caller string) (any, error) {
|
|
ask, _ := json.Marshal(Ask{By: caller, At: p.now()})
|
|
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the ask could not be written: %w", err)
|
|
}
|
|
out := map[string]any{"asked": rev, "says": "the part's process composes and publishes at this ask; grants_user_list's last pass says what it published",
|
|
"controller_grants": NoGrantAgain}
|
|
if raw, prev, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
|
|
out["last_pass"] = map[string]any{"revision": prev, "result": json.RawMessage(raw)}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// UserList answers the tool user-list: the shadow list as last written, beside what the bus node says it loaded now.
|
|
func (p *Part) UserList() (any, error) {
|
|
out := map[string]any{"mode": Shadow}
|
|
raw, rev, found, err := p.Store.Get(StateUserList, KeyList)
|
|
switch {
|
|
case err != nil:
|
|
out["written"] = "not readable: " + err.Error()
|
|
case !found:
|
|
out["written"] = "nothing composed yet: grants_compose_now composes it"
|
|
default:
|
|
var st UserListState
|
|
if err := json.Unmarshal(raw, &st); err != nil {
|
|
return nil, fmt.Errorf("its own user list cannot be read: %w", err)
|
|
}
|
|
out["revision"], out["written"] = rev, st
|
|
if raw, prev, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
|
|
out["last_pass"] = map[string]any{"revision": prev, "result": json.RawMessage(raw)}
|
|
}
|
|
if st.BusNode != "" {
|
|
if raw, found, err := p.Read.Get(ReadHave, st.BusNode+"."+BusModule); err == nil && found {
|
|
if h, err := delivery.DecodeHave(raw); err == nil {
|
|
out["loaded_now"] = h.UserList
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// AssertOne answers the tool assert: one module's buckets, logs and streams as composed from its manifest at the
|
|
// indexer's trunk, beside what the bus holds, and asks the part's process (which alone holds the grant to create) to
|
|
// create what the bus lacks now, even one it created at this manifest and that is gone since (issue 1790). It changes no
|
|
// stream the bus holds. Here, in the tools, it creates nothing itself: the answer names the ask.
|
|
func (p *Part) AssertOne(module string) (any, error) {
|
|
if module == "" {
|
|
return nil, errors.New("assert names a module")
|
|
}
|
|
raw, found, err := p.Read.Get(ReadModules, module)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the indexer's %s cannot be read: %w", module, err)
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("the indexer holds no module %s", module)
|
|
}
|
|
v, err := delivery.DecodeModule(raw)
|
|
if err != nil || v.Manifest == "" {
|
|
return nil, fmt.Errorf("the indexer names no manifest of %s", module)
|
|
}
|
|
m, err := p.cache.get(p.Read, v.Manifest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
held, heldErr := HeldStreams(p.Call)
|
|
a := Assert(module, v.Manifest, m, held, heldErr)
|
|
out := map[string]any{"assertion": a}
|
|
missing := 0
|
|
for _, it := range a.Items {
|
|
if it.Held == nil {
|
|
missing++
|
|
}
|
|
}
|
|
if missing == 0 {
|
|
out["says"] = "the bus holds every bucket, log and stream the manifest declares: nothing to create; a difference is only said"
|
|
return out, nil
|
|
}
|
|
ask, _ := json.Marshal(Ask{By: "grants_assert", At: p.now(), Assert: module})
|
|
rev, err := p.Store.Put(StateUserList, KeyAsked, ask)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the ask to create could not be written: %w", err)
|
|
}
|
|
out["asked"] = rev
|
|
out["says"] = fmt.Sprintf("%d the bus lacks: the part's process creates them at this ask if %s is placed (its grant names only placed "+
|
|
"modules' streams) and says asserted; grants_assert again shows what the bus holds then", missing, module)
|
|
return out, nil
|
|
}
|
|
|
|
// Answer is a tool's arguments read as text.
|
|
func Answer(args map[string]any, name string) string {
|
|
switch v := args[name].(type) {
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
case bool:
|
|
return fmt.Sprint(v)
|
|
}
|
|
return ""
|
|
}
|