The assigner's state and verb placement are assignments, with its events, tools, views and every caller; the assigner copies the retired state at its start (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
// Revoke ends an enrolment at any stage (ADR 0587 §5), by a person's call: at asked it withdraws the ask; at redeeming it also
|
||||
// undoes what redeem had not finished, the vault retiring a node account it already minted, and a node the yes added to
|
||||
// placement is taken off it again. It only takes access away, so it needs no Approve;
|
||||
// assignments is taken off it again. It only takes access away, so it needs no Approve;
|
||||
// it is recorded in the hand-act log. enrolment-revoked carries the key's content and the why; the token is then dropped by
|
||||
// the durable consumer of that event, never before. A revoke that stopped is finished by calling it again.
|
||||
func (h *Holder) Revoke(caller, node, why string) (map[string]any, error) {
|
||||
@@ -42,9 +42,9 @@ func (h *Holder) Revoke(caller, node, why string) (map[string]any, error) {
|
||||
return nil, fmt.Errorf("the vault did not retire the account minted for %s, so nothing is ended (call revoke again): %w", node, err)
|
||||
}
|
||||
}
|
||||
// A node that entered placement on the yes and never confirmed leaves it again (the operator's decision of 2026-11-02 01:05).
|
||||
// A node that entered assignments on the yes and never confirmed leaves it again (the operator's decision of 2026-11-02 01:05).
|
||||
if err := h.unplace(e, "revoked: "+why); err != nil {
|
||||
return nil, fmt.Errorf("%s is not taken off placement, so nothing is ended (call revoke again): %w", node, err)
|
||||
return nil, fmt.Errorf("%s is not taken off assignments, so nothing is ended (call revoke again): %w", node, err)
|
||||
}
|
||||
out := map[string]any{"node": node, "was": e.Stage}
|
||||
if h.Act != nil {
|
||||
@@ -156,12 +156,12 @@ func (h *Holder) answeredYes(ask string) (bool, error) {
|
||||
}
|
||||
|
||||
// endExpired ends an enrolment whose key expired, in this order, each step safe to do again: the node, placed on the yes and
|
||||
// never confirmed, is taken out of placement on the warrant of its ask (only when placement holds it); enrolment-expired names
|
||||
// never confirmed, is taken out of assignments on the warrant of its ask (only when assignments holds it); enrolment-expired names
|
||||
// its issued event (said under one id, so said again is said once); the vault drops the token; the address key goes last, so
|
||||
// a step that failed is found and done on the next pass (ADR 0589 §6).
|
||||
func (h *Holder) endExpired(e Enrolment) error {
|
||||
if err := h.unplace(e, "expired"); err != nil {
|
||||
return fmt.Errorf("%s, whose enrolment expired, is not taken out of placement (tried again on the next pass): %w", e.Node, err)
|
||||
return fmt.Errorf("%s, whose enrolment expired, is not taken out of assignments (tried again on the next pass): %w", e.Node, err)
|
||||
}
|
||||
if err := h.say(EventExpired, expiredEvent(e), eventID(EventExpired, e)); err != nil {
|
||||
return fmt.Errorf("enrolment-expired of %s cannot be said (tried again on the next pass): %w", e.Node, err)
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -31,8 +31,8 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
if _, err := r.h.Revoke("person.operator", "ace", "never confirmed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The node the yes placed, never confirmed, leaves placement on the enrolment's warrant before the end is said.
|
||||
if _, placed := r.rd["mesh-delivery-assigner.placement"]["ace.node"]; placed || len(r.added) == 0 ||
|
||||
// The node the yes placed, never confirmed, leaves assignments on the enrolment's warrant before the end is said.
|
||||
if _, placed := r.rd["mesh-delivery-assigner.assignments"]["ace.node"]; placed || len(r.added) == 0 ||
|
||||
!reflect.DeepEqual(r.added[len(r.added)-1].Change, meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpRemoveNode, Node: "ace"}) || r.added[len(r.added)-1].Warrant.Ask != "ask-ace" {
|
||||
t.Errorf("revoked and still placed: %+v", r.added)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
if err := r.h.Heard(stdio.OpExpire, NodeKey("novox"), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, placed := r.rd["mesh-delivery-assigner.placement"]["novox.node"]; placed {
|
||||
if _, placed := r.rd["mesh-delivery-assigner.assignments"]["novox.node"]; placed {
|
||||
t.Error("expired and still placed")
|
||||
}
|
||||
last := r.said[len(r.said)-1]
|
||||
@@ -65,7 +65,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
t.Errorf("said %s %s: %+v, %v; vault %v, keys %v", last.Event, last.Body, x, err, r.vault.calls, r.keys())
|
||||
}
|
||||
// ADR 0589 §6: an enrolment whose key expired unseen (before the watch handed it over, or while the holder was down) is
|
||||
// found by its address key, which names its ask and issue: it leaves placement on that ask's warrant, enrolment-expired
|
||||
// found by its address key, which names its ask and issue: it leaves assignments on that ask's warrant, enrolment-expired
|
||||
// names its rebuilt issued event, the token is dropped and the address key goes last. A step that fails is done again on
|
||||
// the next pass, the address key kept until then.
|
||||
for _, c := range []struct {
|
||||
@@ -92,11 +92,11 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
}
|
||||
last := r.said[len(r.said)-1]
|
||||
x, err := decodeExpired(last.Body)
|
||||
_, placed := r.rd["mesh-delivery-assigner.placement"][c.node+".node"]
|
||||
_, placed := r.rd["mesh-delivery-assigner.assignments"][c.node+".node"]
|
||||
removed := r.added[len(r.added)-1]
|
||||
if err != nil || x.IssuedID != meshnodeenrollerclient.IssuedID(c.node, issued) || placed || removed.Change.Op != meshmoduleassignerclient.OpRemoveNode ||
|
||||
removed.Change.Node != c.node || removed.Warrant.Ask != "ask-"+c.node || !slices.Contains(r.vault.calls, "drop "+c.node) || r.has(AddressKey("10.9.0.9")) {
|
||||
t.Errorf("%s: said %s (%v), placed %v, last placement call %+v, vault %v, keys %v", c.name, last.Body, err, placed, removed, r.vault.calls, r.keys())
|
||||
t.Errorf("%s: said %s (%v), placed %v, last assignments call %+v, vault %v, keys %v", c.name, last.Body, err, placed, removed, r.vault.calls, r.keys())
|
||||
}
|
||||
}
|
||||
// ADR 0589 §6, the negative rows: an address key the catch-up finds with its enrolment key gone is no expiry when its ask
|
||||
@@ -121,8 +121,8 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
r.asker.answer("ask-"+node, "no")
|
||||
}, false},
|
||||
{"after a refused add", func(r *rig, node string) {
|
||||
delete(r.rd["mesh-delivery-assigner.placement"], node+".node") // the add is tried, and refused
|
||||
r.placeErr = errors.New("mesh-placement did not take the node: the proposal is refused: change 1: an address taken")
|
||||
delete(r.rd["mesh-delivery-assigner.assignments"], node+".node") // the add is tried, and refused
|
||||
r.placeErr = errors.New("mesh-module-assigner did not take the node: the proposal is refused: change 1: an address taken")
|
||||
_ = r.h.Warranted(yes, oc.Warrant{Ask: "ask-" + node})
|
||||
r.asker.answer("ask-"+node, "yes")
|
||||
r.placeErr = nil
|
||||
@@ -142,7 +142,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
stage = StageLive
|
||||
}
|
||||
r.seed("joiner", "10.9.0.11", stage)
|
||||
r.rd["mesh-delivery-assigner.placement"]["joiner.node"] = `{"v":1,"node":"joiner","decided":{"address":"10.9.0.11"},"modules":[]}`
|
||||
r.rd["mesh-delivery-assigner.assignments"]["joiner.node"] = `{"v":1,"node":"joiner","decided":{"address":"10.9.0.11"},"modules":[]}`
|
||||
c.end(r, "joiner")
|
||||
removes := 0
|
||||
for _, a := range r.added {
|
||||
|
||||
@@ -20,7 +20,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},
|
||||
{"DLV-M6 the placement is read from its first view, never from the assigner's state", thePlacementIsReadFromItsFirstView},
|
||||
{"DLV-M6 the assignments is read from its first view, never from the assigner's state", theAssignmentsIsReadFromItsFirstView},
|
||||
} {
|
||||
t.Run(c.name, c.run)
|
||||
}
|
||||
@@ -61,18 +61,18 @@ func theLoadedListIsReadFromTheGrantsPass(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// thePlacementIsReadFromItsFirstView: the holder's placement comes from the picture of the assigner's placement (its first
|
||||
// view and views family, ADR 0615 §1), every other state through the runtime; the runtime is never asked for placement.
|
||||
func thePlacementIsReadFromItsFirstView(t *testing.T) {
|
||||
pics := memPictures{PlacementRead: {"joiner.node": `{"v":1,"node":"joiner","decided":{"address":"10.9.0.11"},"modules":[]}`}}
|
||||
rest := memRead{PlacementRead: {"stale.node": `{"v":1,"node":"stale","modules":[]}`}, "node-engine.have": {}}
|
||||
// theAssignmentsIsReadFromItsFirstView: the holder's assignments comes from the picture of the assigner's assignments (its first
|
||||
// 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": {}}
|
||||
h := &Holder{Read: followedReader{pics: pics, rest: rest}}
|
||||
p, err := h.placements()
|
||||
p, err := h.assignments()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := p["joiner"]; !ok || len(p) != 1 {
|
||||
t.Errorf("placements = %v, want joiner's alone, from the first view", p)
|
||||
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 {
|
||||
t.Errorf("node-engine.have read as %v, %v: want it through the runtime", keys, err)
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Vault is the vault's side of enrolment (ADR 0587 §3, §5): the token and the node's account, and the signatures. The
|
||||
@@ -56,7 +56,7 @@ type Holder struct {
|
||||
Asker Asker
|
||||
// Say publishes a seat event under a message id the bus keeps one of.
|
||||
Say func(event string, body any, id string) error
|
||||
// AddNode asks mesh-placement to add the node, or to take a node that never confirmed off again (ADR 0587 §7).
|
||||
// AddNode asks mesh-module-assigner to add the node, or to take a node that never confirmed off again (ADR 0587 §7).
|
||||
AddNode func(AddNode) error
|
||||
// Warrant and Act record in the hand-act log, through the seat mesh-hand-acts: the operator's warrant for an ask, and an
|
||||
// act done by hand (a revoke).
|
||||
@@ -99,7 +99,7 @@ func (h *Holder) say(event string, body any, id string) error {
|
||||
return h.Say(event, body, id)
|
||||
}
|
||||
|
||||
// AddNode is what the holder asks mesh-placement to add (ADR 0587 §7): the proposal's one change, a joining node with the
|
||||
// AddNode is what the holder asks mesh-module-assigner to add (ADR 0587 §7): the proposal's one change, a joining node with the
|
||||
// address the operator approved and the public halves of its keys, and the issue's warrant, which names this node and this
|
||||
// address (meshmoduleassigner.Warrant).
|
||||
type AddNode struct {
|
||||
@@ -108,10 +108,10 @@ type AddNode struct {
|
||||
Warrant meshmoduleassigner.Warrant
|
||||
}
|
||||
|
||||
// place adds the node into placement, with the address the operator approved and its tunnel key, on the issue's warrant
|
||||
// (ADR 0587 §7; the operator's decision of 2026-11-02 01:05), unless placement holds it at that address already.
|
||||
func (h *Holder) place(e Enrolment, placements map[string]meshmoduleassigner.Placement) error {
|
||||
if p, ok := placements[e.Node]; ok && p.Decided.Address == e.Address {
|
||||
// place adds the node into assignments, with the address the operator approved and its tunnel key, on the issue's warrant
|
||||
// (ADR 0587 §7; the operator's decision of 2026-11-02 01:05), unless assignments holds it at that address already.
|
||||
func (h *Holder) place(e Enrolment, assignments map[string]meshmoduleassigner.Assignments) error {
|
||||
if p, ok := assignments[e.Node]; ok && p.Decided.Address == e.Address {
|
||||
return nil
|
||||
}
|
||||
w, err := h.warrantOf(e)
|
||||
@@ -123,14 +123,14 @@ func (h *Holder) place(e Enrolment, placements map[string]meshmoduleassigner.Pla
|
||||
Keys: &meshmoduleassigner.ProposalChangeKeys{Overlay: e.OverlayKey}}})
|
||||
}
|
||||
|
||||
// unplace takes a node that never confirmed off placement again, on the same warrant, when its enrolment ends revoked, refused
|
||||
// or expired: the hub stops peering a machine that will not join. A node placement does not hold is left as it is.
|
||||
// unplace takes a node that never confirmed off assignments again, on the same warrant, when its enrolment ends revoked, refused
|
||||
// or expired: the hub stops peering a machine that will not join. A node assignments does not hold is left as it is.
|
||||
func (h *Holder) unplace(e Enrolment, why string) error {
|
||||
placements, err := h.placements()
|
||||
assignments, err := h.assignments()
|
||||
if err != nil {
|
||||
return fmt.Errorf("placement cannot be read: %w", err)
|
||||
return fmt.Errorf("assignments cannot be read: %w", err)
|
||||
}
|
||||
if _, ok := placements[e.Node]; !ok {
|
||||
if _, ok := assignments[e.Node]; !ok {
|
||||
return nil
|
||||
}
|
||||
w, err := h.warrantOf(e)
|
||||
@@ -143,7 +143,7 @@ func (h *Holder) unplace(e Enrolment, why string) error {
|
||||
|
||||
func (h *Holder) warrantOf(e Enrolment) (meshmoduleassigner.Warrant, error) {
|
||||
if h.AddNode == nil || h.WarrantOf == nil {
|
||||
return meshmoduleassigner.Warrant{}, errors.New("no route to mesh-placement")
|
||||
return meshmoduleassigner.Warrant{}, errors.New("no route to mesh-module-assigner")
|
||||
}
|
||||
warrant, err := h.WarrantOf(e.Ask)
|
||||
if err != nil {
|
||||
@@ -191,21 +191,21 @@ func (h *Holder) openEnrolments() (map[string]Row, map[string]Enrolment, error)
|
||||
return rows, out, nil
|
||||
}
|
||||
|
||||
// placements reads every node's placement, assembled from its keys.
|
||||
func (h *Holder) placements() (map[string]meshmoduleassigner.Placement, error) {
|
||||
keys, err := h.Read.Keys(PlacementRead)
|
||||
// assignments reads every node's assignments, assembled from its keys.
|
||||
func (h *Holder) assignments() (map[string]meshmoduleassigner.Assignments, error) {
|
||||
keys, err := h.Read.Keys(AssignmentsRead)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("placement cannot be listed: %w", err)
|
||||
return nil, fmt.Errorf("assignments cannot be listed: %w", err)
|
||||
}
|
||||
held := map[string][]byte{}
|
||||
for _, k := range keys {
|
||||
if raw, found, err := h.Read.Get(PlacementRead, k); err != nil {
|
||||
return nil, fmt.Errorf("placement key %s cannot be read: %w", k, err)
|
||||
if raw, found, err := h.Read.Get(AssignmentsRead, k); err != nil {
|
||||
return nil, fmt.Errorf("assignments key %s cannot be read: %w", k, err)
|
||||
} else if found {
|
||||
held[k] = raw
|
||||
}
|
||||
}
|
||||
p, err := meshmoduleassignerclient.PlacementsOf(held)
|
||||
p, err := meshmoduleassignerclient.AssignmentsOf(held)
|
||||
if len(p) == 0 && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,19 +215,19 @@ func (h *Holder) placements() (map[string]meshmoduleassigner.Placement, error) {
|
||||
// firstTunnel is the joining node's first tunnel as the token carries it (ADR 0587 §3, insight of 2026-11-02; as the
|
||||
// controller's `token issue` builds it): the machine's own tunnel key given at issue, its address, the private network's
|
||||
// range (privateRange), the hub's tunnel key from its record (the key its tunnel holds; on an adopted hub
|
||||
// that has not written one, the found tunnel's key kept in its placement), the hub's endpoint from its placement, and the
|
||||
// that has not written one, the found tunnel's key kept in its assignments), the hub's endpoint from its assignments, and the
|
||||
// node's address on the private network. A mesh with no hub, or a hub with no key or no endpoint, has no tunnel to join
|
||||
// through, and that is said.
|
||||
func (h *Holder) firstTunnel(placements map[string]meshmoduleassigner.Placement, address, overlayKey string) (meshnodeenrollerclient.TokenTunnel, error) {
|
||||
var hub *meshmoduleassigner.Placement
|
||||
for _, p := range placements {
|
||||
func (h *Holder) firstTunnel(assignments map[string]meshmoduleassigner.Assignments, address, overlayKey string) (meshnodeenrollerclient.TokenTunnel, error) {
|
||||
var hub *meshmoduleassigner.Assignments
|
||||
for _, p := range assignments {
|
||||
if p.Decided.Hub {
|
||||
p := p
|
||||
hub = &p
|
||||
}
|
||||
}
|
||||
if hub == nil {
|
||||
return meshnodeenrollerclient.TokenTunnel{}, errors.New("placement names no hub, so there is no tunnel to join through")
|
||||
return meshnodeenrollerclient.TokenTunnel{}, errors.New("assignments names no hub, so there is no tunnel to join through")
|
||||
}
|
||||
key := ""
|
||||
if k, err := nodeengineclient.HaveNodeKey(hub.Node); err == nil {
|
||||
@@ -245,9 +245,9 @@ func (h *Holder) firstTunnel(placements map[string]meshmoduleassigner.Placement,
|
||||
missing = append(missing, "no tunnel key in its record")
|
||||
}
|
||||
if hub.Decided.Endpoint == "" {
|
||||
missing = append(missing, "no endpoint in its placement")
|
||||
missing = append(missing, "no endpoint in its assignments")
|
||||
}
|
||||
prefix, rerr := privateRange(placements)
|
||||
prefix, rerr := privateRange(assignments)
|
||||
if len(missing) > 0 {
|
||||
return meshnodeenrollerclient.TokenTunnel{}, fmt.Errorf("the hub %s has %s, so there is no tunnel to join through", hub.Node, strings.Join(missing, " and "))
|
||||
}
|
||||
@@ -271,11 +271,11 @@ func (h *Holder) hasRecord(node string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// privateRange is the private network's range: the hub's tunnel range in its placement, as the controller's `token issue`
|
||||
// writes it into the token today (the adopted tunnel's range). A mesh whose hub's placement names none has no range a token
|
||||
// privateRange is the private network's range: the hub's tunnel range in its assignments, as the controller's `token issue`
|
||||
// writes it into the token today (the adopted tunnel's range). A mesh whose hub's assignments names none has no range a token
|
||||
// can carry, and that is said rather than guessed.
|
||||
func privateRange(placements map[string]meshmoduleassigner.Placement) (netip.Prefix, error) {
|
||||
for _, p := range placements {
|
||||
func privateRange(assignments map[string]meshmoduleassigner.Assignments) (netip.Prefix, error) {
|
||||
for _, p := range assignments {
|
||||
if t := p.Decided.Tunnel; p.Decided.Hub && t != nil && t.Range != "" {
|
||||
r, err := netip.ParsePrefix(t.Range)
|
||||
if err != nil {
|
||||
@@ -284,14 +284,14 @@ func privateRange(placements map[string]meshmoduleassigner.Placement) (netip.Pre
|
||||
return r.Masked(), nil
|
||||
}
|
||||
}
|
||||
return netip.Prefix{}, errors.New("no private network is known: the hub's placement names no tunnel range")
|
||||
return netip.Prefix{}, errors.New("no private network is known: the hub's assignments names no tunnel range")
|
||||
}
|
||||
|
||||
// nextAddress is the lowest address of the private network that no placement key and no reservation holds (ADR 0587 §4),
|
||||
// nextAddress is the lowest address of the private network that no assignments key and no reservation holds (ADR 0587 §4),
|
||||
// after skip. The network is the hub's range; the hub's own address and the tunnel's are never given.
|
||||
func nextAddress(placements map[string]meshmoduleassigner.Placement, reserved map[string]bool, skip map[string]bool) (string, error) {
|
||||
func nextAddress(assignments map[string]meshmoduleassigner.Assignments, reserved map[string]bool, skip map[string]bool) (string, error) {
|
||||
taken := map[string]bool{}
|
||||
for _, p := range placements {
|
||||
for _, p := range assignments {
|
||||
if a := p.Decided.Address; a != "" {
|
||||
taken[a] = true
|
||||
}
|
||||
@@ -305,7 +305,7 @@ func nextAddress(placements map[string]meshmoduleassigner.Placement, reserved ma
|
||||
}
|
||||
}
|
||||
}
|
||||
prefix, err := privateRange(placements)
|
||||
prefix, err := privateRange(assignments)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package main
|
||||
// key-value bucket `enrolments` with a time to live per key (the keys written as the runtime writes them: a create with the
|
||||
// expiry, a put with a message TTL, an update by revision), its events said on their real subjects, the bus's own expiry marker
|
||||
// heard, and the other parts stood in for — the vault (which writes the new node's key of node-logins at node-account, as the
|
||||
// vault does), the operator's channel, placement, and the grants part's rule that a key at live makes the user enrol.<node> and
|
||||
// vault does), the operator's channel, assignments, and the grants part's rule that a key at live makes the user enrol.<node> and
|
||||
// a key gone takes it away.
|
||||
|
||||
import (
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
@@ -195,7 +195,7 @@ func (v *standInVault) Signer() (string, error) { return "vault-key", nil }
|
||||
|
||||
// 774/S60/AC1: issue, a warrant, redeem as enrol.<node> and confirm, whole on a real bus. The user enrol.<node> is composed
|
||||
// while the key is live and leaves with it; the node confirms as the login its key of node-logins names; node-enrolled and
|
||||
// placement-changed are said; the vault is asked to drop the token only after the end; and a token never redeemed is ended by
|
||||
// assignments-changed are said; the vault is asked to drop the token only after the end; and a token never redeemed is ended by
|
||||
// the bus's own expiry, heard, saying enrolment-expired and bringing the drop.
|
||||
func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
srv := testbus.Start(t)
|
||||
@@ -225,9 +225,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
}
|
||||
enrolments := mk(enrolmentsBucket, true)
|
||||
// No controller lease bucket at all: the controller ended by its unassignment (issue 1126).
|
||||
logins, placement := mk(nodeLoginsBucket, false), mk("mesh-delivery-assigner_placement", false)
|
||||
logins, assignments := mk(nodeLoginsBucket, false), mk("mesh-delivery-assigner_assignments", false)
|
||||
have := mk("node-engine_have", false)
|
||||
if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil {
|
||||
if _, err := assignments.Put(ctx, "hub.node", []byte(hubAssignments)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := have.Put(ctx, "hub.node", []byte(hubRecord)); err != nil {
|
||||
@@ -235,12 +235,12 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
}
|
||||
// The seats' events are kept on a stream of the events, as the bus keeps them, and heard on their real subjects.
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_EVENTS",
|
||||
Subjects: []string{"mesh.seat.mesh-enrolment.event.>", "mesh.seat.mesh-placement.event.>"}}); err != nil {
|
||||
Subjects: []string{"mesh.seat.mesh-enrolment.event.>", "mesh.seat.mesh-module-assigner.event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var heardMu sync.Mutex
|
||||
var heard []string
|
||||
for _, subject := range []string{"mesh.seat.mesh-enrolment.event.*", "mesh.seat.mesh-placement.event.*"} {
|
||||
for _, subject := range []string{"mesh.seat.mesh-enrolment.event.*", "mesh.seat.mesh-module-assigner.event.*"} {
|
||||
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
|
||||
heardMu.Lock()
|
||||
heard = append(heard, m.Subject[strings.LastIndex(m.Subject, ".")+1:])
|
||||
@@ -260,19 +260,19 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
_, err := js.Publish(ctx, "mesh.seat.mesh-enrolment.event."+event, raw, jetstream.WithMsgID(id))
|
||||
return err
|
||||
},
|
||||
// mesh-placement, stood in: an add-node writes the node's key, a remove-node deletes it, each saying placement-changed.
|
||||
// mesh-module-assigner, stood in: an add-node writes the node's key, a remove-node deletes it, each saying assignments-changed.
|
||||
AddNode: func(a AddNode) error {
|
||||
if a.Change.Op == meshmoduleassignerclient.OpRemoveNode {
|
||||
if err := placement.Delete(ctx, a.Change.Node+".node"); err != nil {
|
||||
if err := assignments.Delete(ctx, a.Change.Node+".node"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
value := `{"v":1,"node":"` + a.Change.Node + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
|
||||
if _, err := placement.Put(ctx, a.Change.Node+".node", []byte(value)); err != nil {
|
||||
if _, err := assignments.Put(ctx, a.Change.Node+".node", []byte(value)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := js.Publish(ctx, "mesh.seat.mesh-placement.event.placement-changed", []byte(`{}`))
|
||||
_, err := js.Publish(ctx, "mesh.seat.mesh-module-assigner.event.assignments-changed", []byte(`{}`))
|
||||
return err
|
||||
},
|
||||
WarrantOf: func(ask string) (string, error) { return ask + "@2026-11-02T10:00:00Z", nil },
|
||||
@@ -357,9 +357,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
if missing := tok.Missing(); len(missing) != 0 || tok.Tunnel == nil || tok.Tunnel.Key != tunnelKey || tok.Tunnel.Address != a.Address+"/32" {
|
||||
t.Fatalf("the token misses %v: tunnel %+v", missing, tok.Tunnel)
|
||||
}
|
||||
// The node is in placement before redeem: the operator's yes placed it, so the hub peers it before the machine dials.
|
||||
if ace, err := placement.Get(ctx, "ace.node"); err != nil || !strings.Contains(string(ace.Value()), a.Address) {
|
||||
t.Fatalf("ace is not in placement before redeem: %v", err)
|
||||
// The node is in assignments before redeem: the operator's yes placed it, so the hub peers it before the machine dials.
|
||||
if ace, err := assignments.Get(ctx, "ace.node"); err != nil || !strings.Contains(string(ace.Value()), a.Address) {
|
||||
t.Fatalf("ace is not in assignments before redeem: %v", err)
|
||||
}
|
||||
args := redeemArgsWith("ace", 1, "serving-ace", tunnelKey)
|
||||
if _, err := h.Redeem("enrol.somebody", args); err == nil {
|
||||
@@ -381,9 +381,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
if _, err := h.Confirm(nl.Login, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor("node-enrolled and placement-changed are said and the user leaves the list", func() bool {
|
||||
waitFor("node-enrolled and assignments-changed are said and the user leaves the list", func() bool {
|
||||
ev := events()
|
||||
return slices.Contains(ev, EventEnrolled) && slices.Contains(ev, "placement-changed") && !hasUser("enrol.ace")
|
||||
return slices.Contains(ev, EventEnrolled) && slices.Contains(ev, "assignments-changed") && !hasUser("enrol.ace")
|
||||
})
|
||||
waitFor("the vault is asked to drop the token after the end", func() bool {
|
||||
vault.mu.Lock()
|
||||
@@ -411,9 +411,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
if hasUser("enrol.g14") {
|
||||
t.Error("the user enrol.g14 stays after its key expired")
|
||||
}
|
||||
// It never confirmed, so it left placement again before its end was said.
|
||||
if _, err := placement.Get(ctx, "g14.node"); err == nil {
|
||||
t.Error("g14 stays in placement after its enrolment expired")
|
||||
// It never confirmed, so it left assignments again before its end was said.
|
||||
if _, err := assignments.Get(ctx, "g14.node"); err == nil {
|
||||
t.Error("g14 stays in assignments after its enrolment expired")
|
||||
}
|
||||
// Its address key had no expiry of its own: the holder took it last, after the drop (ADR 0589 §6).
|
||||
waitFor("the expired enrolment's address key is deleted", func() bool { keys, _ := store.Keys(); return len(keys) == 0 })
|
||||
@@ -428,7 +428,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asker.answer(b.Ask, "yes") // the router's record of the answer, which the catch-up reads (ADR 0589 §6)
|
||||
if _, err := placement.Get(ctx, "novox.node"); err != nil {
|
||||
if _, err := assignments.Get(ctx, "novox.node"); err != nil {
|
||||
t.Fatalf("novox was not placed on the yes: %v", err)
|
||||
}
|
||||
down.Store(true)
|
||||
@@ -436,14 +436,14 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor("the key is gone", func() bool { _, found, _ := store.Get(NodeKey("novox")); return !found })
|
||||
if _, err := placement.Get(ctx, "novox.node"); err != nil {
|
||||
t.Fatalf("novox left placement while no holder ran: %v", err)
|
||||
if _, err := assignments.Get(ctx, "novox.node"); err != nil {
|
||||
t.Fatalf("novox left assignments while no holder ran: %v", err)
|
||||
}
|
||||
if err := h.Reconcile(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := placement.Get(ctx, "novox.node"); err == nil {
|
||||
t.Error("novox, expired while the holder was down, stays in placement")
|
||||
if _, err := assignments.Get(ctx, "novox.node"); err == nil {
|
||||
t.Error("novox, expired while the holder was down, stays in assignments")
|
||||
}
|
||||
waitFor("the token of novox is dropped and its address key goes", func() bool {
|
||||
vault.mu.Lock()
|
||||
|
||||
@@ -42,7 +42,7 @@ type IssueAnswer struct {
|
||||
}
|
||||
|
||||
// Issue opens an enrolment (ADR 0587 §4): it refuses at once, naming every finding, a name that is no node name or that a
|
||||
// node record, a placement key or an open enrolment holds; chooses the lowest free
|
||||
// node record, an assignments key or an open enrolment holds; chooses the lowest free
|
||||
// address and reserves it by creating its key (the bus refuses a key that exists, so two issues never get one address);
|
||||
// writes the key at stage asked; and asks the operator, on a channel that proves who answered. The token is made only on
|
||||
// the operator's yes (Warranted).
|
||||
@@ -57,7 +57,7 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration
|
||||
if strings.TrimSpace(why) == "" {
|
||||
findings = append(findings, "no why is given: the operator is asked why this machine joins")
|
||||
}
|
||||
// The machine's tunnel key, made there by `nox-mesh-host key`: on the operator's yes the node enters placement with it, so
|
||||
// The machine's tunnel key, made there by `nox-mesh-host key`: on the operator's yes the node enters assignments with it, so
|
||||
// the hub peers it before the machine redeems (the operator's decision of 2026-11-02 01:05).
|
||||
if err := checkTunnelKey(overlayKey); err != nil {
|
||||
findings = append(findings, err.Error())
|
||||
@@ -68,13 +68,13 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration
|
||||
if lasts < 0 || lasts > meshnodeenrollerclient.MostTokenLife {
|
||||
findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, meshnodeenrollerclient.MostTokenLife))
|
||||
}
|
||||
placements, perr := h.placements()
|
||||
assignments, perr := h.assignments()
|
||||
if perr != nil {
|
||||
findings = append(findings, "placement cannot be read: "+perr.Error())
|
||||
findings = append(findings, "assignments cannot be read: "+perr.Error())
|
||||
}
|
||||
if CheckNode(node) == nil {
|
||||
if _, ok := placements[node]; ok {
|
||||
findings = append(findings, node+" has a placement key already")
|
||||
if _, ok := assignments[node]; ok {
|
||||
findings = append(findings, node+" has an assignments key already")
|
||||
}
|
||||
if h.hasRecord(node) {
|
||||
findings = append(findings, node+" has a node record already")
|
||||
@@ -86,7 +86,7 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration
|
||||
}
|
||||
}
|
||||
if perr == nil {
|
||||
if _, err := h.firstTunnel(placements, "0.0.0.0", overlayKey); err != nil {
|
||||
if _, err := h.firstTunnel(assignments, "0.0.0.0", overlayKey); err != nil {
|
||||
findings = append(findings, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration
|
||||
if err != nil {
|
||||
return IssueAnswer{}, err
|
||||
}
|
||||
addr, err := nextAddress(placements, reserved, skip)
|
||||
addr, err := nextAddress(assignments, reserved, skip)
|
||||
if err != nil {
|
||||
return IssueAnswer{}, fmt.Errorf("no enrolment of %q is issued: %w", node, err)
|
||||
}
|
||||
@@ -193,7 +193,7 @@ func (h *Holder) raise(e Enrolment) error {
|
||||
return err
|
||||
}
|
||||
// The address key names the ask and the issue's time from now, with no expiry of its own: it outlives the enrolment key,
|
||||
// so an enrolment that expires unseen can still be taken out of placement on its ask's warrant (ADR 0589 §6).
|
||||
// so an enrolment that expires unseen can still be taken out of assignments on its ask's warrant (ADR 0589 §6).
|
||||
if _, err := h.Store.Put(AddressKey(e.Address), Reservation{Node: e.Node, Ask: e.Ask, Issued: e.Issued}, 0); err != nil {
|
||||
return fmt.Errorf("the address key %s cannot name the ask %s, so the operator is not asked: %w", e.Address, e.Ask, err)
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func (h *Holder) byAsk(ask string) (Enrolment, Row, bool, error) {
|
||||
}
|
||||
|
||||
// Warranted is the operator's answer, handed once by the asker (occ.Client.Listen): a no ends the enrolment; a yes records
|
||||
// the warrant, adds the node to placement with its tunnel key, asks the vault for the token, moves the key to live with the bus's expiry at the token's, and says
|
||||
// the warrant, adds the node to assignments with its tunnel key, asks the vault for the token, moves the key to live with the bus's expiry at the token's, and says
|
||||
// enrolment-issued (ADR 0587 §4, §5).
|
||||
func (h *Holder) Warranted(o oc.Option, w oc.Warrant) error {
|
||||
e, _, found, err := h.byAsk(w.Ask)
|
||||
@@ -245,22 +245,22 @@ func (h *Holder) Warranted(o oc.Option, w oc.Warrant) error {
|
||||
return fmt.Errorf("the warrant %s is not recorded in the hand-act log, so no token is made (mesh-enrolment.again asks again): %w", w.Ask, err)
|
||||
}
|
||||
}
|
||||
placements, err := h.placements()
|
||||
assignments, err := h.assignments()
|
||||
if err != nil {
|
||||
return fmt.Errorf("placement cannot be read, so the token's tunnel is not built (mesh-enrolment.again asks again): %w", err)
|
||||
return fmt.Errorf("assignments cannot be read, so the token's tunnel is not built (mesh-enrolment.again asks again): %w", err)
|
||||
}
|
||||
tunnel, err := h.firstTunnel(placements, e.Address, e.OverlayKey)
|
||||
tunnel, err := h.firstTunnel(assignments, e.Address, e.OverlayKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the token's tunnel is not built (mesh-enrolment.again asks again): %w", err)
|
||||
}
|
||||
// The node enters placement now, with its tunnel key, on this warrant: the hub peers it before the machine redeems over
|
||||
// The node enters assignments now, with its tunnel key, on this warrant: the hub peers it before the machine redeems over
|
||||
// the tunnel (the operator's decision of 2026-11-02 01:05). Only then is the token made.
|
||||
if err := h.place(e, placements); err != nil {
|
||||
if err := h.place(e, assignments); err != nil {
|
||||
if addRefused(err) {
|
||||
// mesh-placement refused the node itself, not the call to it: the enrolment ends (ADR 0589 §6).
|
||||
return h.refuse(e, "mesh-placement refused the node: "+err.Error())
|
||||
// mesh-module-assigner refused the node itself, not the call to it: the enrolment ends (ADR 0589 §6).
|
||||
return h.refuse(e, "mesh-module-assigner refused the node: "+err.Error())
|
||||
}
|
||||
return fmt.Errorf("the node %s is not added to placement, so no token is made (mesh-enrolment.again asks again): %w", e.Node, err)
|
||||
return fmt.Errorf("the node %s is not added to assignments, so no token is made (mesh-enrolment.again asks again): %w", e.Node, err)
|
||||
}
|
||||
if _, err := h.Vault.Token(e.Node, e.Expires, tunnel); err != nil {
|
||||
return fmt.Errorf("the vault did not make the token of %s (mesh-enrolment.again asks the operator again): %w", e.Node, err)
|
||||
@@ -273,7 +273,7 @@ func (h *Holder) Warranted(o oc.Option, w oc.Warrant) error {
|
||||
return h.sayEvent(EventIssued, e, "")
|
||||
}
|
||||
|
||||
// addRefused says mesh-placement answered the add-node with a refusal of the proposal (the assigner's words, "the proposal is
|
||||
// addRefused says mesh-module-assigner answered the add-node with a refusal of the proposal (the assigner's words, "the proposal is
|
||||
// refused: …"), as against a call that did not reach it or was not answered, which `again` asks once more.
|
||||
func addRefused(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "the proposal is refused")
|
||||
@@ -306,7 +306,7 @@ func (h *Holder) refuse(e Enrolment, reason string) error {
|
||||
}
|
||||
// A yes whose token failed added the node already; a refusal takes it off again.
|
||||
if err := h.unplace(e, reason); err != nil {
|
||||
return fmt.Errorf("%s is not taken off placement, so its enrolment stays: %w", e.Node, err)
|
||||
return fmt.Errorf("%s is not taken off assignments, so its enrolment stays: %w", e.Node, err)
|
||||
}
|
||||
if err := h.sayEvent(EventRefused, e, reason); err != nil {
|
||||
return fmt.Errorf("enrolment-refused of %s cannot be said, so its keys stay: %w", e.Node, err)
|
||||
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
var yes = oc.Option{ID: "yes"}
|
||||
|
||||
// 774/S55/AC1: issue refuses, naming every finding, what it must: a bad name, a name a node record, a placement key or an
|
||||
// 774/S55/AC1: issue refuses, naming every finding, what it must: a bad name, a name a node record, an assignments key or an
|
||||
// open enrolment holds, and an expiry past a day; never the controller's lease, which no rig writes (issue 1126).
|
||||
func issueRefusesNamingEveryFinding(t *testing.T) {
|
||||
cases := []struct {
|
||||
@@ -26,22 +26,22 @@ func issueRefusesNamingEveryFinding(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{"a bad name and a long expiry", nil, "Ace.1", 48 * time.Hour, []string{"not a valid node name", "outside zero to 24h0m0s"}},
|
||||
{"a placement key and a node record", func(r *rig) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["ace.node"] = `{"v":1,"node":"ace","modules":[]}`
|
||||
{"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"] = `{}`
|
||||
}, "ace", 0, []string{"ace has a placement key already"}},
|
||||
}, "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) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["hub.node"] = strings.Replace(hubPlacement, `,"range":"10.9.0.0/24"`, "", 1)
|
||||
r.rd["mesh-delivery-assigner.assignments"]["hub.node"] = strings.Replace(hubAssignments, `,"range":"10.9.0.0/24"`, "", 1)
|
||||
}, "ace", -1, []string{"no tunnel key is given", "names no tunnel range"}},
|
||||
// The token carries the first tunnel (ADR 0587 §3, insight of 2026-11-02): no hub, or a hub with no key or endpoint.
|
||||
{"no hub", func(r *rig) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["hub.node"] = strings.Replace(hubPlacement, `"hub":true,`, "", 1)
|
||||
}, "ace", 0, []string{"placement names no hub"}},
|
||||
r.rd["mesh-delivery-assigner.assignments"]["hub.node"] = strings.Replace(hubAssignments, `"hub":true,`, "", 1)
|
||||
}, "ace", 0, []string{"assignments names no hub"}},
|
||||
{"a hub with no key and no endpoint", func(r *rig) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["hub.node"] = strings.Replace(hubPlacement, `"endpoint":"192.0.2.1:51820",`, "", 1)
|
||||
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")
|
||||
}, "ace", 0, []string{"the hub hub has no tunnel key in its record and no endpoint in its placement"}},
|
||||
}, "ace", 0, []string{"the hub hub has no tunnel key in its record and no endpoint in its assignments"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
r := newRig(t)
|
||||
@@ -112,9 +112,9 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
HubKey: "hub-tunnel-key", HubEndpoint: "192.0.2.1:51820"}); got != want {
|
||||
t.Errorf("the token's tunnel %+v, want %+v", got, want)
|
||||
}
|
||||
// The yes added the node to placement with its tunnel key alone, on the warrant, before the token was made.
|
||||
// The yes added the node to assignments with its tunnel key alone, on the warrant, before the token was made.
|
||||
if p, tok := slices.Index(r.vault.calls, "add-node ace"), slices.Index(r.vault.calls, "token ace"); p < 0 || tok < p {
|
||||
t.Errorf("the token was made before placement held the node: %v", r.vault.calls)
|
||||
t.Errorf("the token was made before assignments held the node: %v", r.vault.calls)
|
||||
}
|
||||
for _, a := range r.added {
|
||||
if a.Change.Node == "ace" && (*a.Change.Keys != meshmoduleassigner.ProposalChangeKeys{Overlay: tunnelKey} || a.Warrant.Ask != e.Ask || a.Change.Decided.Address != live.Address) {
|
||||
@@ -138,7 +138,7 @@ func aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(r.keys()) != 0 || r.events()[0] != EventRefused || len(r.added) != 0 {
|
||||
t.Fatalf("keys %v events %v placement %v", r.keys(), r.events(), r.added)
|
||||
t.Fatalf("keys %v events %v assignments %v", r.keys(), r.events(), r.added)
|
||||
}
|
||||
mustContain(t, string(r.said[0].Body), `"address":"`+a.Address+`"`)
|
||||
b, _ := r.h.Issue("person.operator", "g14", "a new machine", tunnelKey, 0)
|
||||
@@ -155,7 +155,7 @@ func aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain
|
||||
t.Fatalf("stage %s", e.Stage)
|
||||
}
|
||||
// The yes placed the node before the vault failed: a refusal now would take it off again (decline, below for ace).
|
||||
if _, placed := r.rd["mesh-delivery-assigner.placement"]["novox.node"]; !placed {
|
||||
if _, placed := r.rd["mesh-delivery-assigner.assignments"]["novox.node"]; !placed {
|
||||
t.Fatal("the yes did not place novox before the vault was asked")
|
||||
}
|
||||
r.now = r.now.Add(time.Minute)
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// memStore is the holder's bucket in memory: revisions, and the expiry each key was written with.
|
||||
@@ -205,7 +205,7 @@ type said struct {
|
||||
Body json.RawMessage
|
||||
}
|
||||
|
||||
// rig is a holder over fakes, with no controller lease at all (issue 1126), one hub in placement and a clock the test moves.
|
||||
// rig is a holder over fakes, with no controller lease at all (issue 1126), one hub in assignments and a clock the test moves.
|
||||
type rig struct {
|
||||
mu sync.Mutex
|
||||
t *testing.T
|
||||
@@ -229,13 +229,13 @@ const hubRecord = `{"v":1,"record":{"v":1,"node":"hub","tunnel-key":"hub-tunnel-
|
||||
// tunnelKey is a machine's tunnel public key as `nox-mesh-host key` prints it.
|
||||
const tunnelKey = "dHVubmVsLWtleS1vZi10aGUtbmV3LW1hY2hpbmUtMDE="
|
||||
|
||||
const hubPlacement = `{"v":1,"node":"hub","decided":{"hub":true,"endpoint":"192.0.2.1:51820","address":"10.9.0.1","tunnel":{"interface":"wg0","address":"10.9.0.1/24","range":"10.9.0.0/24"}},"modules":[]}`
|
||||
const hubAssignments = `{"v":1,"node":"hub","decided":{"hub":true,"endpoint":"192.0.2.1:51820","address":"10.9.0.1","tunnel":{"interface":"wg0","address":"10.9.0.1/24","range":"10.9.0.0/24"}},"modules":[]}`
|
||||
|
||||
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.placement": {"hub.node": hubPlacement},
|
||||
"node-engine.have": {"hub.node": hubRecord},
|
||||
"mesh-delivery-assigner.assignments": {"hub.node": hubAssignments},
|
||||
"node-engine.have": {"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 {
|
||||
@@ -248,12 +248,12 @@ func newRig(t *testing.T) *rig {
|
||||
AddNode: func(a AddNode) error {
|
||||
r.added = append(r.added, a)
|
||||
r.vault.calls = append(r.vault.calls, a.Change.Op+" "+a.Change.Node)
|
||||
if r.placeErr == nil { // placement writes or deletes the node as the assigner does on the warrant
|
||||
if r.placeErr == nil { // assignments writes or deletes the node as the assigner does on the warrant
|
||||
n := a.Change.Node
|
||||
if a.Change.Op == meshmoduleassignerclient.OpRemoveNode {
|
||||
delete(r.rd["mesh-delivery-assigner.placement"], n+".node")
|
||||
delete(r.rd["mesh-delivery-assigner.assignments"], n+".node")
|
||||
} else {
|
||||
r.rd["mesh-delivery-assigner.placement"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
|
||||
r.rd["mesh-delivery-assigner.assignments"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
|
||||
}
|
||||
}
|
||||
return r.placeErr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// mesh-enrolment: the holder of the seat mesh-enrolment (novox/hq ADR 0587). A Go bundle served by a runtime of its own,
|
||||
// on its own bus account (`runs-as`), so that its account alone writes its bucket `enrolments` with per-key expiry. It
|
||||
// asks the operator on their channel, the vault to mint and sign, mesh-placement to add the node and mesh-hand-acts to
|
||||
// asks the operator on their channel, the vault to mint and sign, mesh-module-assigner to add the node and mesh-hand-acts to
|
||||
// record the acts. stdout is the MCP channel; what this module says, it says on stderr.
|
||||
package main
|
||||
|
||||
@@ -86,7 +86,7 @@ func (runtimeReader) Get(state, key string) ([]byte, bool, error) {
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// pictures are the states the enrolment follows rather than reads (views.Reader): the assigner's placement (ADR 0615 §1).
|
||||
// pictures are the states the enrolment follows rather than reads (views.Reader): the assigner's assignments (ADR 0615 §1).
|
||||
type pictures interface {
|
||||
Follows(state string) bool
|
||||
Keys(state string) ([]string, error)
|
||||
@@ -113,19 +113,19 @@ func (r followedReader) Get(state, key string) ([]byte, bool, error) {
|
||||
return r.rest.Get(state, key)
|
||||
}
|
||||
|
||||
// PlacementRead is the assigner's placement as the enrolment reads it, and PlacementTool the assigner's read tool the
|
||||
// AssignmentsRead is the assigner's assignments as the enrolment reads it, and AssignmentsTool the assigner's read tool the
|
||||
// manifest invokes for its first view (ADR 0615, DLV-M6).
|
||||
const (
|
||||
PlacementRead = assignerModule + "." + placementState
|
||||
PlacementTool = assignerModule + ".assigner_read_placement"
|
||||
AssignmentsRead = assignerModule + "." + assignmentsState
|
||||
AssignmentsTool = assignerModule + ".assigner_read_assignments"
|
||||
)
|
||||
|
||||
// followed is the enrolment's pictures of the assigner's placement, started at once: its first view asked through
|
||||
// PlacementTool, then the assigner's views family; past its bound the finding in the enrolment's own findings.
|
||||
// followed is the enrolment's pictures of the assigner's assignments, started at once: its first view asked through
|
||||
// AssignmentsTool, then the assigner's views family; past its bound the finding in the enrolment's own findings.
|
||||
func followed() *views.Reader {
|
||||
v := &views.Reader{Module: Module, Logf: func(f string, a ...any) { logf("["+Module+"] "+f, a...) }, Findings: views.OwnFindings{},
|
||||
Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
||||
v.Follow(assignerModule, placementState, PlacementTool)
|
||||
v.Follow(assignerModule, assignmentsState, AssignmentsTool)
|
||||
v.Start(context.Background())
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ const (
|
||||
signersKey = "key"
|
||||
// nodeLoginsBucket is the vault's state node-logins (the holder reads it through the grants part, not directly).
|
||||
nodeLoginsBucket = vaultModule + "_node-logins"
|
||||
// placementState is the assigner's state placement, which the enrolment reads (reads: through its first view).
|
||||
placementState = "placement"
|
||||
// assignmentsState is the assigner's state assignments, which the enrolment reads (reads: through its first view).
|
||||
assignmentsState = "assignments"
|
||||
)
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be
|
||||
// enrol.<node>, for a key at stage live (or at redeeming with the same keys' digest), checks the proof against the identity key
|
||||
// presented, and then, each step read back and none repeated that is done: spends the token (the key to redeeming, by
|
||||
// compare-and-set, with no expiry) once placement holds the node (written on the operator's yes; the vault reads the node's
|
||||
// compare-and-set, with no expiry) once assignments holds the node (written on the operator's yes; the vault reads the node's
|
||||
// address there) and the tunnel key presented is the one issued for, asks the vault for the node's account and the certificate
|
||||
// of the keys presented,
|
||||
// waits for the bus node to load node.<node>, and answers the whole, signed by the vault. Called again with the same keys it
|
||||
@@ -88,15 +88,15 @@ func (h *Holder) spend(node, digest, overlay string) (Enrolment, error) {
|
||||
if err != nil {
|
||||
return Enrolment{}, err
|
||||
}
|
||||
// The tunnel key presented is the one the operator approved at issue, which placement and the hub already hold.
|
||||
// The tunnel key presented is the one the operator approved at issue, which assignments and the hub already hold.
|
||||
if overlay != e.OverlayKey {
|
||||
return Enrolment{}, fmt.Errorf("the tunnel key presented is not the one %s's enrolment was issued for: the enrolment is not spent", node)
|
||||
}
|
||||
// The node entered placement on the operator's yes; the vault reads its address there.
|
||||
if placements, err := h.placements(); err != nil {
|
||||
return Enrolment{}, fmt.Errorf("placement cannot be read (call redeem again with the same keys): %w", err)
|
||||
} else if p, ok := placements[node]; !ok || p.Decided.Address != e.Address {
|
||||
return Enrolment{}, fmt.Errorf("placement does not hold %s at %s yet (call redeem again with the same keys)", node, e.Address)
|
||||
// The node entered assignments on the operator's yes; the vault reads its address there.
|
||||
if assignments, err := h.assignments(); err != nil {
|
||||
return Enrolment{}, fmt.Errorf("assignments cannot be read (call redeem again with the same keys): %w", err)
|
||||
} else if p, ok := assignments[node]; !ok || p.Decided.Address != e.Address {
|
||||
return Enrolment{}, fmt.Errorf("assignments does not hold %s at %s yet (call redeem again with the same keys)", node, e.Address)
|
||||
}
|
||||
switch e.Stage {
|
||||
case StageRedeeming:
|
||||
|
||||
@@ -25,7 +25,7 @@ func redeemArgsWith(node string, seed byte, serving, overlay string) meshnodeenr
|
||||
}
|
||||
|
||||
// 774/S56/AC1: a redeem from another caller, with a bad proof, after the expiry, or with other keys than the first, is
|
||||
// refused (one row each), and a good one spends the token, writes the node into placement on the issue's warrant and
|
||||
// refused (one row each), and a good one spends the token, writes the node into assignments on the issue's warrant and
|
||||
// answers once the bus node loaded the account.
|
||||
func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
good := redeemArgs("ace", 1, "serving-a")
|
||||
@@ -61,7 +61,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, meshnodeenrollerclient.RedeemProof("ace", a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey)))
|
||||
return a
|
||||
}(), nil, "not the one ace's enrolment was issued for"},
|
||||
{"not in placement yet", "enrol.ace", good, func(r *rig) { delete(r.rd["mesh-delivery-assigner.placement"], "ace.node") }, "placement does not hold ace"},
|
||||
{"not in assignments yet", "enrol.ace", good, func(r *rig) { delete(r.rd["mesh-delivery-assigner.assignments"], "ace.node") }, "assignments does not hold ace"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
r := newRig(t)
|
||||
@@ -76,7 +76,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
}
|
||||
mustContain(t, err.Error(), c.want)
|
||||
if len(r.vault.calls) != 0 || len(r.added) != 0 {
|
||||
t.Errorf("%s: the vault %v or placement %v was called", c.name, r.vault.calls, r.added)
|
||||
t.Errorf("%s: the vault %v or assignments %v was called", c.name, r.vault.calls, r.added)
|
||||
}
|
||||
}
|
||||
r := newRig(t)
|
||||
@@ -90,9 +90,9 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
if e.Stage != StageRedeeming || e.KeysDigest != good.KeysDigest() || r.st.ttl[NodeKey("ace")] != 0 {
|
||||
t.Errorf("spent %+v, ttl %v", e, r.st.ttl[NodeKey("ace")])
|
||||
}
|
||||
// Redeem writes no placement: the yes did. It binds the identity keys, which the vault checks against the account's digest.
|
||||
// Redeem writes no assignments: the yes did. It binds the identity keys, which the vault checks against the account's digest.
|
||||
if len(r.added) != 0 || ans.Signature == "" || ans.Account != "account-ace" || ans.Login != "node.ace" || ans.Signer != "vault-signer" || ans.Certificate != "PEM-serving-a" {
|
||||
t.Errorf("placement %+v, answer %+v", r.added, ans)
|
||||
t.Errorf("assignments %+v, answer %+v", r.added, ans)
|
||||
}
|
||||
// The certificate's call carries every key presented, which the vault checks against the account's digest.
|
||||
if meshnodeenrollerclient.KeysDigest(r.vault.cert.Identity, r.vault.cert.Overlay, r.vault.cert.Sealing, r.vault.cert.Serving) != good.KeysDigest() {
|
||||
@@ -104,7 +104,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 774/S56/AC2: a redeem stopped after the vault's call, or after the placement call, called again with the same keys,
|
||||
// 774/S56/AC2: a redeem stopped after the vault's call, or after the assignments call, called again with the same keys,
|
||||
// finishes and answers the same, and the vault minted once (it is asked for the same account).
|
||||
func aRedeemStoppedMidwayIsFinishedByTheSameKeysAgain(t *testing.T) {
|
||||
good := redeemArgs("ace", 1, "serving-a")
|
||||
@@ -126,17 +126,17 @@ func aRedeemStoppedMidwayIsFinishedByTheSameKeysAgain(t *testing.T) {
|
||||
t.Errorf("%s: %v, answer %+v, want %+v", stop, err, got, want)
|
||||
}
|
||||
if len(r.added) != 0 {
|
||||
t.Errorf("redeem wrote placement %d times", len(r.added))
|
||||
t.Errorf("redeem wrote assignments %d times", len(r.added))
|
||||
}
|
||||
}
|
||||
// Placement that already holds the node at the approved address is not written again.
|
||||
// Assignments that already holds the node at the approved address is not written again.
|
||||
r := newRig(t)
|
||||
r.seed("ace", "10.9.0.2", StageRedeeming)
|
||||
e, _ := r.enrolment("ace")
|
||||
e.KeysDigest = good.KeysDigest()
|
||||
_, _ = r.st.Put(NodeKey("ace"), e, 0)
|
||||
r.rd["mesh-delivery-assigner.placement"]["ace.node"] = `{"v":1,"node":"ace","decided":{"address":"10.9.0.2"},"modules":[]}`
|
||||
r.rd["mesh-delivery-assigner.assignments"]["ace.node"] = `{"v":1,"node":"ace","decided":{"address":"10.9.0.2"},"modules":[]}`
|
||||
if _, err := r.h.Redeem("enrol.ace", good); err != nil || len(r.added) != 0 {
|
||||
t.Errorf("%v, placement written %d times", err, len(r.added))
|
||||
t.Errorf("%v, assignments written %d times", err, len(r.added))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func (r *rig) seed(node, address string, stage string) {
|
||||
_, _ = r.st.Put(NodeKey(node), e, 0)
|
||||
_, _ = r.st.Put(AddressKey(address), Reservation{Node: node, Ask: e.Ask, Issued: e.Issued}, 0)
|
||||
if stage != StageAsked { // the operator's yes placed it, with its tunnel key
|
||||
r.rd["mesh-delivery-assigner.placement"][node+".node"] = `{"v":1,"node":"` + node + `","decided":{"address":"` + address + `"},"modules":[]}`
|
||||
r.rd["mesh-delivery-assigner.assignments"][node+".node"] = `{"v":1,"node":"` + node + `","decided":{"address":"` + address + `"},"modules":[]}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,28 +98,28 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
if slices.Contains(m.Reads, "mesh-controller.lease") {
|
||||
t.Error("the manifest reads the controller's lease, which the code no longer reads (issue 1126)")
|
||||
}
|
||||
// The assigner's placement is followed, never read (ADR 0615 §1, DLV-M6): its read tool invoked, its views family consumed.
|
||||
// The assigner's assignments is followed, never read (ADR 0615 §1, DLV-M6): its read tool invoked, its views family consumed.
|
||||
for _, r := range m.Reads {
|
||||
if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") {
|
||||
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(m.Invokes, PlacementTool) || !slices.Equal(m.Tools, []string{"enrolment_first_views", "enrolment_first_view_again"}) {
|
||||
t.Errorf("invokes %v, tools %v: want %s and the first-view tools", m.Invokes, m.Tools, PlacementTool)
|
||||
if !slices.Contains(m.Invokes, AssignmentsTool) || !slices.Equal(m.Tools, []string{"enrolment_first_views", "enrolment_first_view_again"}) {
|
||||
t.Errorf("invokes %v, tools %v: want %s and the first-view tools", m.Invokes, m.Tools, AssignmentsTool)
|
||||
}
|
||||
// The holders the holder names beside its own are the manifest's (D-11): the vault's signers it reads, the seats it invokes.
|
||||
if !slices.Contains(m.Reads, signersRead) || !slices.Contains(m.Invokes, placementAdd) || !slices.Contains(m.Invokes, vaultSign) ||
|
||||
if !slices.Contains(m.Reads, signersRead) || !slices.Contains(m.Invokes, assignmentsAdd) || !slices.Contains(m.Invokes, vaultSign) ||
|
||||
!slices.Contains(m.Invokes, vaultToken) || !slices.Contains(m.Invokes, vaultAccount) {
|
||||
t.Errorf("reads %v, invokes %v: want %s, %s, %s, %s and %s", m.Reads, m.Invokes, signersRead, placementAdd, vaultSign, vaultToken, vaultAccount)
|
||||
t.Errorf("reads %v, invokes %v: want %s, %s, %s, %s and %s", m.Reads, m.Invokes, signersRead, assignmentsAdd, vaultSign, vaultToken, vaultAccount)
|
||||
}
|
||||
if enrolmentsBucket != m.Module+"_"+State || !strings.HasPrefix(PlacementTool, assignerModule+".") {
|
||||
t.Errorf("the names %s, %s are not the manifest's module %s and the assigner %s", enrolmentsBucket, PlacementTool, m.Module, assignerModule)
|
||||
if enrolmentsBucket != m.Module+"_"+State || !strings.HasPrefix(AssignmentsTool, assignerModule+".") {
|
||||
t.Errorf("the names %s, %s are not the manifest's module %s and the assigner %s", enrolmentsBucket, AssignmentsTool, m.Module, assignerModule)
|
||||
}
|
||||
var heard []string
|
||||
for _, e := range EndEvents {
|
||||
heard = append(heard, Seat+"."+e)
|
||||
}
|
||||
heard = append(heard, assignerModule+".views."+placementState)
|
||||
heard = append(heard, assignerModule+".views."+assignmentsState)
|
||||
if !slices.Equal(m.Consumes, heard) {
|
||||
t.Errorf("consumes %v, the end events are %v", m.Consumes, heard)
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ func Tools(h *Holder) []stdio.Tool {
|
||||
{Name: IssueTool,
|
||||
Description: "Open an enrolment: let a new machine join the mesh (novox/hq ADR 0587 §4). Takes the node's name, a why and an " +
|
||||
"expiry (one hour unless given, at most 24 hours). Refused at once, naming every finding, for a name that is no node name " +
|
||||
"or that a node record, a placement key or an open enrolment holds. Otherwise it chooses the lowest free private-network address, reserves it, " +
|
||||
"or that a node record, an assignments key or an open enrolment holds. Otherwise it chooses the lowest free private-network address, reserves it, " +
|
||||
"writes the enrolment at stage asked and asks the operator on a channel that proves who answered; only their yes adds the " +
|
||||
"node to placement with the machine's tunnel key (overlay_key, as `nox-mesh-host key` prints it there), so the hub peers " +
|
||||
"node to assignments with the machine's tunnel key (overlay_key, as `nox-mesh-host key` prints it there), so the hub peers " +
|
||||
"it, and then makes the token, which carries the first tunnel (the vault makes it, this seat never holds it). The caller is the one the runtime proves.",
|
||||
Input: obj(map[string]any{"node": str("the new machine's name"), "why": str("why this machine joins"),
|
||||
"expires": str("how long the token lasts, e.g. 1h (default) up to 24h"),
|
||||
@@ -65,7 +65,7 @@ func Tools(h *Holder) []stdio.Tool {
|
||||
{Name: RedeemTool,
|
||||
Description: "The joining machine's one call (novox/hq ADR 0587 §5, §6), answered only for a caller the runtime proves to be " +
|
||||
"enrol.<node>: its node-engine presents the public halves of the keys it made where it runs and a proof signed with its " +
|
||||
"identity key, and the tunnel key it was issued for. The token is spent (once, with those keys) once placement holds the " +
|
||||
"identity key, and the tunnel key it was issued for. The token is spent (once, with those keys) once assignments holds the " +
|
||||
"node (placed on the operator's yes), the vault gives the node's account and signs its certificate, and the answer is given once the bus node loaded node.<node>, " +
|
||||
"signed by the vault. Called again with the same keys it finishes what is not done and answers the same.",
|
||||
Input: obj(map[string]any{"node": str("the machine's name"), "public_key": str("its identity key, base64"),
|
||||
@@ -91,7 +91,7 @@ func Tools(h *Holder) []stdio.Tool {
|
||||
{Name: RevokeTool,
|
||||
Description: "End an enrolment at any stage (novox/hq ADR 0587 §5): at asked it withdraws the ask; at live or redeeming it deletes " +
|
||||
"the key and its address key, and at redeeming the vault retires an account it already minted; a node the yes placed " +
|
||||
"and that never confirmed is taken off placement again. It only takes access away, so it needs no Approve; it is recorded as an act done by hand. Says " +
|
||||
"and that never confirmed is taken off assignments again. It only takes access away, so it needs no Approve; it is recorded as an act done by hand. Says " +
|
||||
"enrolment-revoked; the vault then drops the token. A machine stuck at redeeming is ended by this. A revoke that stopped is " +
|
||||
"finished by calling it again.",
|
||||
Input: obj(map[string]any{"node": str("the machine"), "why": str("why, kept with the act")}, "node", "why"),
|
||||
|
||||
@@ -7,19 +7,19 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Seats the holder calls (ADR 0587 §3, §5, §7). The vault's two verbs are the ones its manifest names this seat the only
|
||||
// caller of; its `sign` names this seat for a node's certificate and a redeem answer.
|
||||
const (
|
||||
vaultToken = "seat:mesh-vault." + mv.VerbEnrolmentToken
|
||||
vaultAccount = "seat:mesh-vault." + mv.VerbNodeAccount
|
||||
vaultSign = "seat:mesh-vault." + mv.VerbSign
|
||||
placementAdd = "seat:" + meshmoduleassigner.Seat + "." + meshmoduleassigner.VerbPropose
|
||||
vaultToken = "seat:mesh-vault." + mv.VerbEnrolmentToken
|
||||
vaultAccount = "seat:mesh-vault." + mv.VerbNodeAccount
|
||||
vaultSign = "seat:mesh-vault." + mv.VerbSign
|
||||
assignmentsAdd = "seat:" + meshmoduleassigner.Seat + "." + meshmoduleassigner.VerbPropose
|
||||
)
|
||||
|
||||
// Call asks a seat verb by its address (stdio.Ask through the runtime, or a test's).
|
||||
@@ -99,10 +99,10 @@ func (v vaultOver) Signer() (string, error) {
|
||||
return k.Key, nil
|
||||
}
|
||||
|
||||
// addNodeOver asks mesh-placement to add the node with the issue's warrant (ADR 0587 §7).
|
||||
// addNodeOver asks mesh-module-assigner to add the node with the issue's warrant (ADR 0587 §7).
|
||||
func addNodeOver(call Call) func(AddNode) error {
|
||||
return func(a AddNode) error {
|
||||
_, err := call(placementAdd, map[string]any{"why": a.Why, "changes": []any{a.Change}, "warrant": a.Warrant})
|
||||
_, err := call(assignmentsAdd, map[string]any{"why": a.Why, "changes": []any{a.Change}, "warrant": a.Warrant})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mesh-module-assigner did not take the node %s: %w", a.Change.Node, err)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ require (
|
||||
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"mesh-node-enroller.enrolment-refused",
|
||||
"mesh-node-enroller.enrolment-revoked",
|
||||
"mesh-node-enroller.enrolment-expired",
|
||||
"mesh-delivery-assigner.views.placement"
|
||||
"mesh-delivery-assigner.views.assignments"
|
||||
],
|
||||
"uses": [
|
||||
"operator-channel"
|
||||
@@ -33,7 +33,7 @@
|
||||
"seat:mesh-vault.enrolment-token",
|
||||
"seat:mesh-vault.node-account",
|
||||
"seat:mesh-vault.sign",
|
||||
"mesh-delivery-assigner.assigner_read_placement"
|
||||
"mesh-delivery-assigner.assigner_read_assignments"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-grants.user-list",
|
||||
|
||||
Reference in New Issue
Block a user