diff --git a/internal/inventory/busrecords.go b/internal/inventory/busrecords.go index 3fa241b..431e5cc 100644 --- a/internal/inventory/busrecords.go +++ b/internal/inventory/busrecords.go @@ -49,6 +49,12 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) { } } + // Who holds each seat held once for the mesh, where the mesh recorded it (novox/hq issue 218). + holdings, err := i.Holdings(ctx) + if err != nil { + return broker.Records{}, fmt.Errorf("cannot read who holds the mesh's seats: %w", err) + } + out := broker.Records{Assigned: map[string][]broker.Declared{}, People: map[string][]string{}, Interchangeable: map[string]bool{}} for _, n := range nodes { @@ -72,7 +78,9 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) { "%s is assigned to %s and is not in the catalogue, so what it may say cannot "+ "be derived", module, n.Name) } - out.Assigned[n.Name] = append(out.Assigned[n.Name], declaredFor(m, seats)) + d := declaredFor(m, seats) + d.Holds = heldHere(d.Holds, holdings, n.Name, module) + out.Assigned[n.Name] = append(out.Assigned[n.Name], d) if m.Instances == catalogue.InstancesInterchangeable { out.Interchangeable[m.Module] = true } @@ -183,3 +191,33 @@ func (i *Inventory) NodesWithALiveToken(ctx context.Context) ([]string, error) { } return out, rows.Err() } + +// heldHere keeps of what a module claims only the seats it holds on this machine (novox/hq issue 218). +// A seat held once per machine is held by every assignment that claims it. A seat held once for the +// mesh is held by one assignment: where the mesh recorded who holds it, a claim on any other machine +// grants nothing and issues nothing — or the module would serve the role's verbs from a machine that +// is not the role's, and a question to the mesh's store would be answered from the wrong database. A +// mesh seat with no holder on record is left as it was derived. +func heldHere(claimed []broker.Seat, holdings []catalogue.Held, node, module string) []broker.Seat { + recorded := map[string][]catalogue.Held{} + for _, h := range holdings { + if h.Scope == catalogue.ScopeMesh { + recorded[h.Claim] = append(recorded[h.Claim], h) + } + } + var out []broker.Seat + for _, s := range claimed { + holders, onRecord := recorded[s.Name] + if s.Scope != catalogue.ScopeMesh || !onRecord { + out = append(out, s) + continue + } + for _, h := range holders { + if h.Node == node && h.Module == module { + out = append(out, s) + break + } + } + } + return out +} diff --git a/internal/inventory/heldhere_test.go b/internal/inventory/heldhere_test.go new file mode 100644 index 0000000..831208c --- /dev/null +++ b/internal/inventory/heldhere_test.go @@ -0,0 +1,32 @@ +package inventory + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/broker" + "github.com/novox/mesh-controller/internal/catalogue" +) + +// novox/hq issue 218: a seat held once for the mesh is granted and issued only to the holder on record; +// a node seat to every machine's claimant; a mesh seat with no holder on record as derived. +func TestOnlyTheRecordedHolderHoldsAMeshSeat(t *testing.T) { + claimed := []broker.Seat{ + {Name: "mesh-store", Scope: catalogue.ScopeMesh}, + {Name: "node-packet-filter", Scope: catalogue.ScopeNode}, + {Name: "unrecorded", Scope: catalogue.ScopeMesh}, + } + holdings := []catalogue.Held{{Claim: "mesh-store", Scope: catalogue.ScopeMesh, Node: "control", Module: "postgres"}} + names := func(ss []broker.Seat) (out []string) { + for _, s := range ss { + out = append(out, s.Name) + } + return + } + if got := names(heldHere(claimed, holdings, "control", "postgres")); len(got) != 3 { + t.Errorf("the holder lost a seat: %v", got) + } + got := names(heldHere(claimed, holdings, "other", "postgres")) + if len(got) != 2 || got[0] != "node-packet-filter" || got[1] != "unrecorded" { + t.Errorf("a claimant on another machine holds %v; want the node seat and the unrecorded one, not the store", got) + } +}