Grants compose the holders on record through mesh-sdk's SeatsToPassOn, so a seat whose holder's manifest left it is granted to its only claimant (issue 1966, ADR 0660)
Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
@@ -1281,6 +1281,91 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// Issue 1966, ADR 0660: the live case. The assigner's record still names the catalogue module as the holder of
|
||||
// mesh-catalog, whose manifest claims it no more, and the indexer claims it: the rule of mesh-sdk (the node-engine's
|
||||
// too) passes the seat to the indexer, the user list grants the seat's permissions to it, the pass is said, and the list
|
||||
// is not held back as the loss of the seat's last holder.
|
||||
t.Run("a holder on record whose manifest left the seat passes it to its only claimant", func(t *testing.T) {
|
||||
seat, ok := catalogue.SeatNamed("mesh-catalog")
|
||||
if !ok {
|
||||
t.Fatal("mesh-catalog is not a seat")
|
||||
}
|
||||
var verbs []string
|
||||
for _, v := range seat.Serves {
|
||||
verbs = append(verbs, v.Name)
|
||||
}
|
||||
claimer := func(module string) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: module, Version: "1", RunsAs: module, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/" + module + "/broker"}}, Claims: []catalogue.Claim{{Name: "mesh-catalog", Scope: catalogue.ScopeMesh, Serves: verbs}},
|
||||
Provides: []catalogue.Offer{{Name: seat.Delivers, Scope: catalogue.ScopeMesh}}}
|
||||
}
|
||||
left := catalogue.Manifest{Module: "mesh-catalog", Version: "1", RunsAs: "mesh-catalog", OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh-catalog/broker"}}}
|
||||
rows := []struct {
|
||||
name string
|
||||
running map[string]catalogue.Manifest // by `<node>.<module>`
|
||||
extra []string // more claimants, assigned on node b
|
||||
want []string // the modules holding the seat afterwards
|
||||
said string
|
||||
}{
|
||||
{"the trunk of the holder dropped the seat", nil, nil, []string{"mesh-delivery-indexer"}, "composed as held by mesh-delivery-indexer"},
|
||||
{"the holder's running build still claims it", map[string]catalogue.Manifest{"a.mesh-catalog": claimer("mesh-catalog")}, nil, []string{"mesh-catalog"}, ""},
|
||||
{"a second claimant", nil, []string{"other"}, nil, "seat mesh-catalog --to <node>/<module>"},
|
||||
}
|
||||
for _, row := range rows {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
in := grants.Inputs{Nodes: []string{"a", "b"},
|
||||
Assigned: map[string][]string{"a": {"mesh-catalog"}, "b": {"mesh-delivery-indexer"}},
|
||||
Manifests: map[string]catalogue.Manifest{"mesh-catalog": left, "mesh-delivery-indexer": claimer("mesh-delivery-indexer")},
|
||||
Running: map[string]catalogue.Manifest{},
|
||||
Placed: map[string]meshmoduleassigner.Assignments{
|
||||
"a": {Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "mesh-catalog"}},
|
||||
Holds: []meshmoduleassigner.Holding{{Claim: "mesh-catalog", Scope: "mesh", Module: "mesh-catalog"}}},
|
||||
"b": {Node: "b", Modules: []meshmoduleassigner.PlacedModule{{Module: "mesh-delivery-indexer"}}}}}
|
||||
maps.Copy(in.Running, row.running)
|
||||
for _, c := range row.extra {
|
||||
in.Manifests[c] = claimer(c)
|
||||
in.Assigned["b"] = append(in.Assigned["b"], c)
|
||||
}
|
||||
before := in.Placed
|
||||
recs, gaps := grants.ListRecords(in)
|
||||
users, refusedU, err := grants.ComposeEach(recs)
|
||||
if len(refusedU) != 0 {
|
||||
t.Fatalf("refused %v", refusedU)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := grants.HoldingsOf(in)
|
||||
var got []string
|
||||
for _, u := range users {
|
||||
if h.ByUsers([]grants.User{u})["mesh-catalog"] {
|
||||
_, module, _ := strings.Cut(u.User, ".")
|
||||
got = append(got, module)
|
||||
}
|
||||
}
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, row.want) {
|
||||
t.Errorf("granted the seat's permissions to %v, want %v", got, row.want)
|
||||
}
|
||||
var said []string
|
||||
for _, g := range gaps {
|
||||
said = append(said, g.Why)
|
||||
}
|
||||
if row.said == "" && len(gaps) != 0 || !strings.Contains(strings.Join(said, "\n"), row.said) {
|
||||
t.Errorf("the pass is said as %q, want %q", said, row.said)
|
||||
}
|
||||
// The in-force list carries the seat (granted to the holder on record); the composed one must too, or the
|
||||
// list is held back as the last holder's loss. Two claimants grant none, and that is held back and said.
|
||||
lost := h.LastHolders(map[string]bool{"mesh-catalog": true}, h.ByUsers(users))
|
||||
if (len(lost) != 0) != (len(row.want) == 0) {
|
||||
t.Errorf("the seat's last holder would be lost: %v", lost)
|
||||
}
|
||||
if !reflect.DeepEqual(before, in.Placed) {
|
||||
t.Errorf("the assigner's record was written: %+v", in.Placed)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 1126 (ADR 0549 §1, §3): once the catalogue places the nodes, a seat held once for the mesh is held where assignments
|
||||
// says, not on every machine running a module that claims it; what assignments adds is composed beside the assignments.
|
||||
t.Run("a mesh seat is held where assignments says", func(t *testing.T) {
|
||||
|
||||
@@ -3,11 +3,11 @@ package grants
|
||||
import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"mesh-grants/internal/buscompose"
|
||||
)
|
||||
@@ -50,6 +50,10 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
out.BusState[module] = m.BusState
|
||||
}
|
||||
}
|
||||
// The holders on record, after the seats whose holder has left are passed (ADR 0660): the one rule of mesh-sdk, the
|
||||
// node-engine's too, read with the manifest each node runs, else the trunk's. Composed, never recorded.
|
||||
holders, passed := holdersAfterPass(in)
|
||||
gaps = append(gaps, passed...)
|
||||
values := func(provider, setting string) []any {
|
||||
var vs []any
|
||||
for _, node := range in.Nodes {
|
||||
@@ -83,7 +87,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
declare := func(m catalogue.Manifest) buscompose.Declared {
|
||||
d := declaredFor(m, seats, declarers)
|
||||
if len(in.Placed) > 0 {
|
||||
d.Holds = heldWherePlaced(d.Holds, in.Placed, node, m.Module)
|
||||
d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module)
|
||||
}
|
||||
for _, sr := range catalogue.ReadsGranted(m, here, node) {
|
||||
d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key})
|
||||
@@ -421,31 +425,71 @@ func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node
|
||||
return out, touches, onEvents
|
||||
}
|
||||
|
||||
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
|
||||
// holds are the holders on record, as catalogue.World's Holdings are: a seat held once for the mesh that some node's
|
||||
// assignments holds is held only where assignments names it (this node's naming this module), so a module claiming it on
|
||||
// two machines holds it on the one assignments names, not on both. A seat no assignments holds is held by derivation, by the
|
||||
// module claiming it wherever it is placed, as it was before assignments: the seeded assignments hold only the seats the
|
||||
// controller had on record (twelve on the control-node), and treating their holds as every holder left nine seats with
|
||||
// no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every node is held wherever the module runs.
|
||||
func heldWherePlaced(holds []buscompose.Seat, placed map[string]meshmoduleassigner.Assignments, node, module string) []buscompose.Seat {
|
||||
same := func(x meshmoduleassigner.Holding, h buscompose.Seat) bool {
|
||||
return (x.Scope == "" || x.Scope == catalogue.ScopeMesh) && sameSeat(x.Claim, h.Name) && x.Kind == h.Kind
|
||||
// holdersAfterPass is the holders on record (each node's assignments, as the seat mesh-module-assigner holds them) as they
|
||||
// stand after catalogue.SeatsToPassOn (novox/hq ADR 0660): a holder whose every assignment's manifest no longer claims
|
||||
// its seat has left it, and the seat is the only claimant's. Each assignment is read with the manifest its node runs
|
||||
// (Inputs.Running), else the trunk's, else the running build of a held module; one with none is unread and keeps what it
|
||||
// holds. A pass is said as a gap and recorded nowhere (ADR 0660 §4). Pure.
|
||||
func holdersAfterPass(in Inputs) ([]catalogue.Held, []Gap) {
|
||||
var held []catalogue.Held
|
||||
for _, node := range slices.Sorted(maps.Keys(in.Placed)) {
|
||||
for _, x := range in.Placed[node].Holds {
|
||||
if x.Scope != "" && x.Scope != catalogue.ScopeMesh {
|
||||
continue
|
||||
}
|
||||
held = append(held, catalogue.Held{Claim: x.Claim, Scope: catalogue.ScopeMesh, Node: node, Module: x.Module, Site: x.Site, Kind: x.Kind})
|
||||
}
|
||||
}
|
||||
var assignments []catalogue.Assignment
|
||||
for _, node := range slices.Sorted(maps.Keys(in.Assigned)) {
|
||||
for _, module := range in.Assigned[node] {
|
||||
m, ok := in.Running[node+"."+module]
|
||||
if !ok {
|
||||
m, ok = in.Manifests[module]
|
||||
}
|
||||
if !ok {
|
||||
m, ok = in.Held[module]
|
||||
}
|
||||
if !ok {
|
||||
m = catalogue.Manifest{Module: module}
|
||||
}
|
||||
assignments = append(assignments, catalogue.Assignment{Node: node, Manifest: m, Unread: !ok})
|
||||
}
|
||||
}
|
||||
passes, said := catalogue.SeatsToPassOn(held, assignments)
|
||||
var gaps []Gap
|
||||
for _, p := range passes {
|
||||
held = slices.DeleteFunc(held, func(h catalogue.Held) bool { return h.Kind == p.From.Kind && sameSeat(h.Claim, p.Seat.Name) })
|
||||
p.To.Site = p.From.Site
|
||||
held = append(held, p.To)
|
||||
gaps = append(gaps, Gap{Input: "seat " + p.Seat.Name, Why: fmt.Sprintf(
|
||||
"%s on %s is on record as its holder and its manifest no longer claims it: composed as held by %s on %s, not recorded (ADR 0660)",
|
||||
p.From.Module, p.From.Node, p.To.Module, p.To.Node)})
|
||||
}
|
||||
for _, w := range said {
|
||||
gaps = append(gaps, Gap{Input: "seat passes", Why: w})
|
||||
}
|
||||
return held, gaps
|
||||
}
|
||||
|
||||
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
|
||||
// holds are the holders on record, as catalogue.World's Holdings are, after the passes of holdersAfterPass: a seat held
|
||||
// once for the mesh that some node's assignments holds is held only where assignments names it (this node's naming this
|
||||
// module), so a module claiming it on two machines holds it on the one assignments names, not on both. A seat no
|
||||
// assignments holds is held by derivation, by the module claiming it wherever it is placed, as it was before assignments:
|
||||
// the seeded assignments hold only the seats the controller had on record (twelve on the control-node), and treating their
|
||||
// holds as every holder left nine seats with no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every
|
||||
// node is held wherever the module runs.
|
||||
func heldWherePlaced(holds []buscompose.Seat, holders []catalogue.Held, node, module string) []buscompose.Seat {
|
||||
same := func(x catalogue.Held, h buscompose.Seat) bool { return sameSeat(x.Claim, h.Name) && x.Kind == h.Kind }
|
||||
var out []buscompose.Seat
|
||||
for _, h := range holds {
|
||||
if h.Scope != "" && h.Scope != catalogue.ScopeMesh {
|
||||
out = append(out, h)
|
||||
continue
|
||||
}
|
||||
onRecord := false
|
||||
for _, p := range placed {
|
||||
if slices.ContainsFunc(p.Holds, func(x meshmoduleassigner.Holding) bool { return same(x, h) }) {
|
||||
onRecord = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !onRecord || slices.ContainsFunc(placed[node].Holds, func(x meshmoduleassigner.Holding) bool { return same(x, h) && x.Module == module }) {
|
||||
onRecord := slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) })
|
||||
if !onRecord || slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) && x.Node == node && x.Module == module }) {
|
||||
out = append(out, h)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user