diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment_test.go index fc99d21b..26e2b67f 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment_test.go @@ -1,8 +1,12 @@ package main import ( + "encoding/json" + "fmt" "os" "testing" + + "git.novox.be/novox/mesh-sdk/go/seatlist" ) // The module's unit tests, as two tables (the repository's test budget): the operator's side (issue, the tokens verb, the @@ -20,6 +24,7 @@ func TestIssueTokensAndTheManifest(t *testing.T) { {"S54/AC1 tokens answers each stage and relays the sealed token of one node", tokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode}, {"S54/AC2 the manifest says what the code does", theManifestSaysWhatTheCodeDoes}, {"S56 the bus node's loaded list is read from the grants part's pass", theLoadedListIsReadFromTheGrantsPass}, + {"A-210 a seat's state is read at the seat's holder", aSeatsStateIsReadAtItsHolder}, {"DLV-M6 the assignments is read from its first view, never from the assigner's state", theAssignmentsIsReadFromItsFirstView}, } { t.Run(c.name, c.run) @@ -65,7 +70,7 @@ func theLoadedListIsReadFromTheGrantsPass(t *testing.T) { // view and views family, ADR 0615 §1), every other state through the runtime; the runtime is never asked for assignments. func theAssignmentsIsReadFromItsFirstView(t *testing.T) { pics := memPictures{AssignmentsRead: {"joiner.node": `{"v":1,"node":"joiner","decided":{"address":"10.9.0.11"},"modules":[]}`}} - rest := memRead{AssignmentsRead: {"stale.node": `{"v":1,"node":"stale","modules":[]}`}, "node-engine.have": {}} + rest := memRead{AssignmentsRead: {"stale.node": `{"v":1,"node":"stale","modules":[]}`}, haveRead: {}} h := &Holder{Read: followedReader{pics: pics, rest: rest}} p, err := h.assignments() if err != nil { @@ -74,7 +79,7 @@ func theAssignmentsIsReadFromItsFirstView(t *testing.T) { if _, ok := p["joiner"]; !ok || len(p) != 1 { t.Errorf("assignments = %v, want joiner's alone, from the first view", p) } - if keys, err := h.Read.Keys("node-engine.have"); err != nil || len(keys) != 0 { + if keys, err := h.Read.Keys(haveRead); err != nil || len(keys) != 0 { t.Errorf("node-engine.have read as %v, %v: want it through the runtime", keys, err) } } @@ -87,3 +92,23 @@ func (p memPictures) Keys(state string) ([]string, error) { return memRead(p).Keys(state) } func (p memPictures) Get(state, key string) ([]byte, bool, error) { return memRead(p).Get(state, key) } + +// A read in the seat form `seat:.` is a read of the state of the module the seat list names as the seat's holder +// (design 47 G A-210, A-128): the vault's signers and the node-engine's have are asked of the modules that hold their seats, whatever +// those are called, and a state named otherwise is read as it is. +func aSeatsStateIsReadAtItsHolder(t *testing.T) { + call := func(key string, _ any) (json.RawMessage, error) { + if key != seatlist.HolderKey() { + return nil, fmt.Errorf("asked %s", key) + } + return json.RawMessage(`{"seats":[{"seat":"mesh-vault","holders":[{"module":"another-vault"}]},{"seat":"node-engine","holders":[]}]}`), nil + } + for state, want := range map[string]string{signersRead: "another-vault.signers", "mesh-config.resolved": "mesh-config.resolved"} { + if got, err := resolveState(call, state); err != nil || got != want { + t.Errorf("%s is read as %q, %v; want %q", state, got, err, want) + } + } + if got, err := resolveState(call, haveRead); err == nil { + t.Errorf("%s with no holder is read as %q, want an error naming the seat", haveRead, got) + } +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go index 7040109e..af6d9f49 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go @@ -231,7 +231,7 @@ func (h *Holder) firstTunnel(assignments map[string]meshmoduleassigner.Assignmen } key := "" if k, err := nodeengineclient.HaveNodeKey(hub.Node); err == nil { - if raw, found, err := h.Read.Get("node-engine.have", k); err == nil && found { + if raw, found, err := h.Read.Get(haveRead, k); err == nil && found { if hn, err := nodeengineclient.DecodeHaveNode(raw); err == nil && hn.Record != nil { key = hn.Record.TunnelKey } @@ -259,7 +259,7 @@ func (h *Holder) firstTunnel(assignments map[string]meshmoduleassigner.Assignmen // hasRecord says the node already writes a record (a node of the mesh, ADR 0587 §4: "a node record ... holds it"). func (h *Holder) hasRecord(node string) bool { - keys, err := h.Read.Keys("node-engine.have") + keys, err := h.Read.Keys(haveRead) if err != nil { return false } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go index d14fa415..e047220f 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go @@ -225,7 +225,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { } enrolments := mk(Module+"_"+enrolmentsState, true) // the runtime's bucket of the bed's credential's module // No controller lease bucket at all: the controller ended by its unassignment (issue 1126). - logins, assignments := mk(nodeLoginsBucket, false), mk("mesh-delivery-assigner_assignments", false) + logins, assignments := mk("mesh-vault_node-logins", false), mk("mesh-delivery-assigner_assignments", false) have := mk("node-engine_have", false) if _, err := assignments.Put(ctx, "hub.node", []byte(hubAssignments)); err != nil { t.Fatal(err) diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go index a4283480..512e4cd0 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go @@ -28,7 +28,7 @@ func issueRefusesNamingEveryFinding(t *testing.T) { {"a bad name and a long expiry", nil, "Ace.1", 48 * time.Hour, []string{"not a valid node name", "outside zero to 24h0m0s"}}, {"an assignments key and a node record", func(r *rig) { r.rd["mesh-delivery-assigner.assignments"]["ace.node"] = `{"v":1,"node":"ace","modules":[]}` - r.rd["node-engine.have"]["ace"] = `{}` + r.rd[haveRead]["ace"] = `{}` }, "ace", 0, []string{"ace has an assignments key already"}}, {"an open enrolment", func(r *rig) { r.seed("ace", "10.9.0.2", StageLive) }, "ace", 0, []string{"an enrolment of ace is open already"}}, {"no tunnel key and no range", func(r *rig) { @@ -40,7 +40,7 @@ func issueRefusesNamingEveryFinding(t *testing.T) { }, "ace", 0, []string{"assignments names no hub"}}, {"a hub with no key and no endpoint", func(r *rig) { r.rd["mesh-delivery-assigner.assignments"]["hub.node"] = strings.Replace(hubAssignments, `"endpoint":"192.0.2.1:51820",`, "", 1) - delete(r.rd["node-engine.have"], "hub.node") + delete(r.rd[haveRead], "hub.node") }, "ace", 0, []string{"the hub hub has no tunnel key in its record and no endpoint in its assignments"}}, } for _, c := range cases { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go index 2a4025b3..723ca658 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go @@ -235,7 +235,7 @@ func newRig(t *testing.T) *rig { r := &rig{t: t, st: newMem(), vault: &fakeVault{fail: map[string]bool{}}, asker: &fakeAsker{}, now: time.Date(2026, 11, 2, 10, 0, 0, 0, time.UTC), loaded: true} r.rd = memRead{ "mesh-delivery-assigner.assignments": {"hub.node": hubAssignments}, - "node-engine.have": {"hub.node": hubRecord}, + haveRead: {"hub.node": hubRecord}, } r.h = &Holder{Store: r.st, Read: r.rd, Vault: r.vault, Asker: r.asker, AskLasts: time.Hour, Say: func(event string, body any, id string) error { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go index 8cd07317..34ddd403 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go @@ -10,12 +10,14 @@ import ( "errors" "fmt" "os" + "strings" "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" ) @@ -74,13 +76,39 @@ func (runtimeStore) Put(key string, v any, ttl time.Duration) (uint64, error) { func (runtimeStore) Delete(key string) error { return stdio.State(State).Delete(key) } func (runtimeStore) Keys() ([]string, error) { return stdio.State(State).Keys() } -// runtimeReader reads another module's state through the runtime. -type runtimeReader struct{} +// runtimeReader reads another module's state through the runtime: a state in the seat form `seat:.` in the state of +// the module that holds the seat when it is read, as the manifest's `reads` names it (design 47 G A-210, A-128). +type runtimeReader struct{ call Call } -func (runtimeReader) Keys(state string) ([]string, error) { return stdio.State(state).Keys() } +// resolveState is the state a read names, a seat's turned into `.` by the seat list's holder of the seat; any +// other name is returned as it is. +func resolveState(call Call, state string) (string, error) { + seatState, ok := strings.CutPrefix(state, "seat:") + if !ok { + return state, nil + } + seat, name, _ := strings.Cut(seatState, ".") + module, err := seatlist.HolderOf(seatlist.HolderCaller(call), seat) + if err != nil { + return "", err + } + return module + "." + name, nil +} -func (runtimeReader) Get(state, key string) ([]byte, bool, error) { - e, err := stdio.State(state).Get(key) +func (r runtimeReader) Keys(state string) ([]string, error) { + name, err := resolveState(r.call, state) + if err != nil { + return nil, err + } + return stdio.State(name).Keys() +} + +func (r runtimeReader) Get(state, key string) ([]byte, bool, error) { + name, err := resolveState(r.call, state) + if err != nil { + return nil, false, err + } + e, err := stdio.State(name).Get(key) if err != nil || e == nil { return nil, false, err } @@ -159,7 +187,7 @@ func SeatEventSubject(event string) string { return "mesh.seat." + Seat + ".even func newHolder(v *views.Reader) *Holder { call := Call(stdio.Ask) rec := meshhandactsclient.Recorder{Ask: stdio.Ask} - rd := followedReader{pics: v, rest: runtimeReader{}} + rd := followedReader{pics: v, rest: runtimeReader{call: call}} client := occ.New("asked") return &Holder{ Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: client, diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/names.go b/modules/mesh-enrolment/cmd/mesh-enrolment/names.go index 8d5ee523..b22b397c 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/names.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/names.go @@ -1,22 +1,25 @@ package main -import "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" +import ( + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" + mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" +) // The holders, states and buckets the enrolment names beside its own, taken from its manifest (module.json: its module, its // `reads`, its state, the seats it invokes); a unit row checks them (TestTheNamesAreTheManifests). No contract names a // holder (novox/hq ADR 0625 §4, ADR 0634 §1). const ( - vaultModule = "mesh-vault" assignerModule = "mesh-delivery-assigner" // enrolmentsState is the holder's state of open enrolments; its bucket is the runtime's, of the credential's module // (design 47 G A-296), never named here. enrolmentsState = meshnodeenroller.StateEnrolments - // signersRead is the vault's state signers as the enrolment's manifest reads it, signersKey the key holding its signing key. - signersRead = vaultModule + ".signers" + // signersRead is the seat mesh-vault's state signers as the enrolment's manifest reads it (`seat:.`, resolved at + // the seat's holder when it is read, design 47 G A-210, A-128), signersKey the key holding its signing key; haveRead is the + // seat node-engine's state have the same way. + signersRead = "seat:" + mv.Seat + ".signers" signersKey = "key" - // nodeLoginsBucket is the vault's state node-logins (the holder reads it through the grants part, not directly). - nodeLoginsBucket = vaultModule + "_node-logins" + haveRead = "seat:node-engine.have" // assignmentsState is the assigner's state assignments, which the enrolment reads (reads: through its first view). assignmentsState = "assignments" ) diff --git a/modules/mesh-enrolment/module.json b/modules/mesh-enrolment/module.json index 1750442f..5bb0e097 100644 --- a/modules/mesh-enrolment/module.json +++ b/modules/mesh-enrolment/module.json @@ -34,12 +34,13 @@ "seat:mesh-vault.enrolment-token", "seat:mesh-vault.node-account", "seat:mesh-vault.sign", - "mesh-delivery-assigner.assigner_read_assignments" + "mesh-delivery-assigner.assigner_read_assignments", + "seat:mesh-module-assigner.assignments" ], "reads": [ "mesh-grants.user-list", - "mesh-vault.signers", - "node-engine.have" + "seat:mesh-vault.signers", + "seat:node-engine.have" ], "state": [ {