diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index 9e48b6a..ec6fb24 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -1133,4 +1133,30 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) { if failed.Verdict == nil || !strings.Contains(strings.Join(failed.Verdict.Fails, "; "), "laptop (offline) did not answer") { t.Errorf("the silent node's verdict = %+v", failed.Verdict) } + + // 774/S59 (ADR 0587 §7): once the lease is handed away, a joining node is written on the enrolment's warrant with no node to + // answer and no ask raised, and placement-changed carries that warrant. + b.putJSON(t, buscompose.LeaseBucket, buscompose.ControllerLeaseKey, []byte(`{"handed-to":"mesh-grants","handed-by":"operator"}`)) + for len(heard) > 0 { + <-heard + } + joined, err := verbs.doPropose(map[string]any{"why": "enrolment of ace", "warrant": `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}`, + "changes": `[{"op":"add-node","node":"ace","decided":{"address":"10.9.0.2"},"keys":{"identity":"aWQ=","sealing":"c2Vh"}}]`}, "novox.mesh-enrolment") + if err != nil { + t.Fatal(err) + } + select { + case e := <-heard: + if e.Proposal.ID != joined.(proposed).Proposal.ID || e.Warrant == nil || e.Warrant.Warrant != "enrol-ace-1@2026-10-28T20:00:00Z" || len(e.Nodes) != 1 || e.Nodes[0].Node != "ace" { + t.Errorf("placement-changed for the joining node = %+v", e) + } + case <-time.After(15 * time.Second): + t.Fatal("no placement-changed heard for the joining node") + } + if ace, err := b.buckets[delivery.PlacementBucket].Get(b.ctx, "ace.node"); err != nil || !strings.Contains(string(ace.Value()), "10.9.0.2") { + t.Errorf("the joining node's key = %v, %v", ace, err) + } + if akey, _ := delivery.PlacementAskKey(joined.(proposed).Proposal.ID); b.hasKey(delivery.AssignerAsksBucket, akey) || b.hasKey(delivery.PlacementProposalsBucket, joined.(proposed).Proposal.ID) { + t.Error("an ask was raised for the joining node, or its proposal stays open") + } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go index c8ef722..3dc3358 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat.go @@ -268,6 +268,10 @@ func (p *passer) stepProposal(ctx context.Context, prop delivery.PlacementPropos p.endProposal(ctx, prop, "adopt and hand-back are gone since the lease write: placement is the mesh's alone") return } + if prop.Warrant != nil { + p.addNode(ctx, prop, revs) + return + } akey, _ := delivery.PlacementAskKey(prop.ID) again := !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At)) if prop.State == delivery.ProposalAsked { @@ -544,6 +548,40 @@ func (p *passer) warranted(ctx context.Context, prop delivery.PlacementProposal, p.followWrite(prop, answer, changes) } +// addNode writes a joining node on the warrant of the enrolment that issued its token (ADR 0587 §7): the operator's Approve +// was the issue's, so no node is waited on and no ask is raised. The key is created at the revision it was laid over (none: a +// node with a key is no joining node); the enrolment recorded the warrant in the hand-act log when it was given, so this does +// not record it again; placement-changed carries the warrant. A key written meanwhile ends the proposal, saying so. +func (p *passer) addNode(ctx context.Context, prop delivery.PlacementProposal, revs map[string]uint64) { + current, _ := p.seatPlacements() + if moved(prop, revs) { + p.endProposal(ctx, prop, "a key it was laid over was written since: the enrolment proposes the node again") + return + } + laid, findings := delivery.LayChanges(current, prop.Changes) + node := prop.Changes[0].Node + value, ok := laid[node] + if len(findings) > 0 || !ok { + p.endProposal(ctx, prop, "the node cannot be added: "+strings.Join(findings, "; ")) + return + } + rev, err := p.writeNode(ctx, node, &value) + if err != nil { + p.endProposal(ctx, prop, "the node's key was not written: "+err.Error()) + return + } + c := delivery.ChangeOf(delivery.Placement{Node: node}, value) + c.Before, c.After = prop.Base[node], rev + p.deleteCandidates(ctx, prop.ID) + p.deletePulses(ctx, prop.ID) + _ = p.kvs[delivery.PlacementProposalsBucket].Delete(ctx, prop.ID) + p.w.put(delivery.PlacementProposalsBucket, prop.ID, nil) + w := prop.Warrant + p.sayChanged(prop, delivery.Answer{Option: assign.OptionYes, Warrant: w.Warrant, + Words: fmt.Sprintf("the operator approved the machine %s at %s through the enrolment's ask %s", w.Node, w.Address, w.Ask)}, + []delivery.PlacementChange{c}) +} + // followWrite follows the keys a warrant wrote: the warrant recorded and placement-changed said. No verb of the // controller is called: the placement is the one source of where modules run (ADR 0567 §8, issue 774). func (p *passer) followWrite(prop delivery.PlacementProposal, answer delivery.Answer, changes []delivery.PlacementChange) { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go index 15a565e..2ef4889 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go @@ -217,9 +217,16 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) { if err != nil { return nil, err } + warrant, err := warrantOf(args["warrant"]) + if err != nil { + return nil, err + } now := s.now() p, laid, findings := assign.Propose(assign.ProposeIn{ID: delivery.NewProposalID(now), By: by, Why: arg(args, "why"), At: now, - Changes: changes, Current: current, Revisions: revs, Held: held, Recorded: recorded}) + Changes: changes, Current: current, Revisions: revs, Held: held, Recorded: recorded, Warrant: warrant}) + if refusal := delivery.AddNodeRefusal(p, moduleOfCaller(by)); refusal != "" { + findings = append(findings, refusal) + } if len(findings) > 0 { return nil, fmt.Errorf("the proposal is refused: %s", strings.Join(findings, "; ")) } @@ -229,6 +236,40 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) { return proposed{Proposal: p, Follow: followProposal(p.ID)}, nil } +// warrantOf reads propose's warrant, an enrolment's for an add-node: an object, given as one or as its text; none is none. +func warrantOf(v any) (*delivery.AddNodeWarrant, error) { + var raw []byte + switch w := v.(type) { + case nil: + return nil, nil + case string: + if strings.TrimSpace(w) == "" { + return nil, nil + } + raw = []byte(w) + default: + var err error + if raw, err = json.Marshal(w); err != nil { + return nil, err + } + } + var out delivery.AddNodeWarrant + dec := json.NewDecoder(strings.NewReader(string(raw))) + dec.DisallowUnknownFields() + if err := dec.Decode(&out); err != nil { + return nil, fmt.Errorf("the warrant is no enrolment's warrant {node, address, ask, warrant}: %w", err) + } + return &out, nil +} + +// moduleOfCaller is the module a caller is, as the runtime proves it: a module's bus user is `.`. +func moduleOfCaller(by string) string { + if _, module, ok := strings.Cut(by, "."); ok { + return module + } + return by +} + // doWithdraw is the verb withdraw (§2): the proposal is marked, and the assigner deletes it and its candidates and says // placement-withdrawn carrying it whole. func (s *seatTools) doWithdraw(args map[string]any, by string) (any, error) { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 6e1d736..82142d1 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -150,6 +150,27 @@ func TestTheSeatsVerbs(t *testing.T) { if _, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op"); err != nil { t.Errorf("a hold: %v", err) } + // 774/S59/AC1 (ADR 0587 §7): an add-node from mesh-enrolment whose warrant names this node and this address is proposed, with + // the node a joining one with no record; from another caller, naming another node or address, or with no warrant it is refused. + addNode := func(by, warrant string) error { + _, err := s.doPropose(map[string]any{"why": "enrolment of ace", "warrant": warrant, "changes": []any{map[string]any{"op": "add-node", "node": "ace", + "decided": map[string]any{"address": "10.9.0.2"}, "keys": map[string]any{"identity": "aWQ=", "sealing": "c2Vh"}}}}, by) + return err + } + good := `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}` + for _, c := range []struct{ by, warrant, want string }{ + {"novox.mesh-grants", good, "mesh-enrolment's alone"}, + {"novox.mesh-enrolment", `{"node":"g14","address":"10.9.0.2","ask":"a","warrant":"a@t"}`, "carries the warrant of g14"}, + {"novox.mesh-enrolment", `{"node":"ace","address":"10.9.0.9","ask":"a","warrant":"a@t"}`, "carries the warrant of ace at 10.9.0.9"}, + {"novox.mesh-enrolment", "", "carries no warrant"}, + } { + if err := addNode(c.by, c.warrant); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("an add-node by %s with warrant %q: %v, want a refusal saying %q", c.by, c.warrant, err, c.want) + } + } + if err := addNode("novox.mesh-enrolment", good); err != nil { + t.Errorf("an add-node from mesh-enrolment: %v", err) + } if _, err := s.doHandBack(map[string]any{"why": "back"}, "op"); err == nil || !strings.Contains(err.Error(), "since the lease write") { t.Errorf("a hand-back: %v", err) } diff --git a/modules/mesh-delivery-assigner/internal/assign/seat.go b/modules/mesh-delivery-assigner/internal/assign/seat.go index 435ac4e..3b2dc1e 100644 --- a/modules/mesh-delivery-assigner/internal/assign/seat.go +++ b/modules/mesh-delivery-assigner/internal/assign/seat.go @@ -24,6 +24,8 @@ type ProposeIn struct { Revisions map[string]uint64 Held func(module string) bool Recorded func(node string) bool + // Warrant is the enrolment's warrant an add-node carries (ADR 0587 §7); only that change has one. + Warrant *delivery.AddNodeWarrant } // Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision), @@ -33,7 +35,7 @@ type ProposeIn struct { // record). A key whose value the proposal leaves as it is changes nothing and is not laid. func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Placement, []string) { p := delivery.PlacementProposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(), - Changes: in.Changes, Base: map[string]uint64{}, State: delivery.ProposalAnswering} + Changes: in.Changes, Warrant: in.Warrant, Base: map[string]uint64{}, State: delivery.ProposalAnswering} var findings []string if p.By == "" { findings = append(findings, "the runtime names no caller: a proposal is made by someone") @@ -75,6 +77,18 @@ func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Pla if recorded == nil { recorded = func(string) bool { return false } } + // A joining node writes its record only once it holds its account, which the add-node's write gives it: this one change by + // this one caller is the exception to a node with no record (ADR 0587 §7, changing ADR 0567 §2). + joining := map[string]bool{} + for _, c := range in.Changes { + if c.Op == delivery.OpAddNode { + joining[c.Node] = true + } + } + if len(joining) > 0 { + was := recorded + recorded = func(node string) bool { return joining[node] || was(node) } + } for _, r := range delivery.CheckPlacements(all, held, recorded) { findings = append(findings, r) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go index 7da3442..f6dbdde 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go @@ -55,7 +55,9 @@ type Holder struct { // 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). Warrant func(ask string) error - Act func(what, why, cause string) error + // WarrantOf is the warrant's id as the teller wrote it (the ask's id and when), read from the router's record of the ask. + WarrantOf func(ask string) (string, error) + Act func(what, why, cause string) error // Loaded says whether the bus node loaded the bus user (ADR 0587 §3: redeem answers once node. is in its list). Loaded func(user string) (bool, error) Now func() time.Time @@ -91,32 +93,13 @@ 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 node, its address, the public halves of its -// keys, and the issue's warrant, which names this node and this address. +// AddNode is what the holder asks mesh-placement 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 (delivery.AddNodeWarrant). type AddNode struct { - Node string `json:"node"` - Address string `json:"address"` - Why string `json:"why"` - Keys NodeKeys `json:"keys"` - Warrant Warrant `json:"warrant"` -} - -// NodeKeys are the public halves of a joining node's keys: identity, private network, sealing and serving, and the -// tunnel it found, as delivery.Redeem presents them. -type NodeKeys struct { - Public string `json:"public_key"` - Overlay string `json:"overlay_key,omitempty"` - Sealing string `json:"sealing_key,omitempty"` - Serving string `json:"serving_key,omitempty"` - Tunnel json.RawMessage `json:"tunnel,omitempty"` -} - -// Warrant is the issue's warrant as the proposal carries it: the ask the operator answered, the node and the address it -// named. -type Warrant struct { - Ask string `json:"ask"` - Node string `json:"node"` - Address string `json:"address"` + Why string + Change delivery.PlacementProposalChange + Warrant delivery.AddNodeWarrant } // issuing says whether issue is on: only while the controller's lease is handed to a holder that is no controller (ADR 0587 diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go index 4325087..3ace80b 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go @@ -211,15 +211,17 @@ func newRig(t *testing.T) *rig { AddNode: func(a AddNode) error { r.added = append(r.added, a) if r.placeErr == nil { // placement writes the node as the assigner does on the warrant - r.rd["mesh-delivery-assigner.placement"][a.Node+".node"] = `{"v":1,"node":"` + a.Node + `","decided":{"address":"` + a.Address + `"},"modules":[]}` + n := a.Change.Node + r.rd["mesh-delivery-assigner.placement"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}` } return r.placeErr }, - Warrant: func(ask string) error { r.warr = append(r.warr, ask); return nil }, - Act: func(what, why, cause string) error { r.acts = append(r.acts, what); return nil }, - Loaded: func(string) (bool, error) { return r.loaded, nil }, - Now: func() time.Time { return r.now }, - Logf: func(f string, a ...any) { t.Logf(f, a...) }} + Warrant: func(ask string) error { r.warr = append(r.warr, ask); return nil }, + WarrantOf: func(ask string) (string, error) { return ask + "@2026-11-02T10:00:00Z", nil }, + Act: func(what, why, cause string) error { r.acts = append(r.acts, what); return nil }, + Loaded: func(string) (bool, error) { return r.loaded, nil }, + Now: func() time.Time { return r.now }, + Logf: func(f string, a ...any) { t.Logf(f, a...) }} return r } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go index 99cb6b0..a9ae270 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go @@ -92,13 +92,15 @@ func newHolder() *Holder { call := Call(stdio.Ask) rec := handacts.Recorder{Ask: stdio.Ask} rd := runtimeReader{} + client := asker.New("asked") return &Holder{ - Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: asker.New("asked"), + Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: client, Say: func(event string, body any, id string) error { return stdio.SeatPublish(SeatEventSubject(event), body, id) }, - AddNode: addNodeOver(call), - Warrant: func(ask string) error { _, err := rec.RecordWarrant(Module, ask); return err }, + AddNode: addNodeOver(call), + WarrantOf: warrantOf(client), + Warrant: func(ask string) error { _, err := rec.RecordWarrant(Module, ask); return err }, Act: func(what, why, cause string) error { _, err := rec.Record(handacts.Request{What: what, Why: why, Cause: cause}) return err @@ -110,6 +112,20 @@ func newHolder() *Holder { } } +// warrantOf is the warrant id the teller writes, `@`, read from the router's record of the ask. +func warrantOf(c *asker.Client) func(string) (string, error) { + return func(ask string) (string, error) { + state, w, err := c.Where(ask) + if err != nil { + return "", err + } + if w == nil { + return "", fmt.Errorf("the router records the ask %s as %q, with no warrant", ask, state) + } + return ask + "@" + w.At.UTC().Format(time.RFC3339Nano), nil + } +} + // loadedVia says a bus user is loaded when the grants part's last pass lists it among the composed users and the bus node's // loaded list holds it (its credentials row says loaded). func loadedVia(rd Reader) func(string) (bool, error) { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go index e2a01a0..38f38af 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go @@ -10,11 +10,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// keysOf are the public halves a redeem presented. -func keysOf(a delivery.Redeem) NodeKeys { - return NodeKeys{Public: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey, Tunnel: a.Tunnel} -} - // Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be // enrol., 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 @@ -50,7 +45,7 @@ func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer if err != nil { return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err) } - if err := h.place(e, keysOf(a)); err != nil { + if err := h.place(e, a); err != nil { return delivery.RedeemAnswer{}, err } if h.Loaded != nil { @@ -122,7 +117,7 @@ func (h *Holder) spend(node, digest string) (Enrolment, error) { } // place writes the node into placement with the issue's warrant, unless placement holds it at the approved address already. -func (h *Holder) place(e Enrolment, keys NodeKeys) error { +func (h *Holder) place(e Enrolment, a delivery.Redeem) error { placements, err := h.placements() if err != nil { return fmt.Errorf("placement cannot be read (call redeem again with the same keys): %w", err) @@ -130,11 +125,17 @@ func (h *Holder) place(e Enrolment, keys NodeKeys) error { if p, ok := placements[e.Node]; ok && p.Decided.Address == e.Address { return nil } - if h.AddNode == nil { + if h.AddNode == nil || h.WarrantOf == nil { return errors.New("no route to mesh-placement") } - if err := h.AddNode(AddNode{Node: e.Node, Address: e.Address, Why: "enrolment of " + e.Node + ": " + e.Why, Keys: keys, - Warrant: Warrant{Ask: e.Ask, Node: e.Node, Address: e.Address}}); err != nil { + warrant, err := h.WarrantOf(e.Ask) + if err != nil { + return fmt.Errorf("the operator's warrant for %s cannot be read from the router (call redeem again with the same keys): %w", e.Ask, err) + } + change := delivery.PlacementProposalChange{Op: delivery.OpAddNode, Node: e.Node, Decided: &delivery.NodeDecided{Address: e.Address}, + Keys: &delivery.JoiningKeys{Identity: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey}} + if err := h.AddNode(AddNode{Why: "enrolment of " + e.Node + ": " + e.Why, Change: change, + Warrant: delivery.AddNodeWarrant{Node: e.Node, Address: e.Address, Ask: e.Ask, Warrant: warrant}}); err != nil { return fmt.Errorf("%w (call redeem again with the same keys)", err) } return nil diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go index 5d173d9..9dad26f 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go @@ -77,7 +77,8 @@ func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) { if e.Stage != StageRedeeming || e.KeysDigest != good.KeysDigest() || r.st.ttl[NodeKey("ace")] != 0 || r.st.ttl[AddressKey("10.9.0.2")] != 0 { t.Errorf("spent %+v, ttl %v/%v", e, r.st.ttl[NodeKey("ace")], r.st.ttl[AddressKey("10.9.0.2")]) } - if len(r.added) != 1 || r.added[0].Warrant != (Warrant{Ask: "ask-ace", Node: "ace", Address: "10.9.0.2"}) || ans.Signature == "" || ans.Account != "account-ace" || ans.Login != "node.ace" || ans.Signer != "vault-signer" || ans.Certificate != "PEM-serving-a" { + if len(r.added) != 1 || r.added[0].Warrant != (delivery.AddNodeWarrant{Ask: "ask-ace", Node: "ace", Address: "10.9.0.2", Warrant: "ask-ace@2026-11-02T10:00:00Z"}) || + r.added[0].Change.Keys.Digest() != good.KeysDigest() || r.added[0].Change.Decided.Address != "10.9.0.2" || 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) } r.loaded = false diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go index 29c1f65..7ff7953 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go @@ -95,12 +95,9 @@ func (v vaultOver) Signer() (string, error) { // addNodeOver asks mesh-placement 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{map[string]any{"op": "add-node", "node": a.Node, "address": a.Address, "keys": a.Keys, "warrant": a.Warrant}}, - }) + _, err := call(placementAdd, map[string]any{"why": a.Why, "changes": []any{a.Change}, "warrant": a.Warrant}) if err != nil { - return fmt.Errorf("mesh-placement did not take the node %s: %w", a.Node, err) + return fmt.Errorf("mesh-placement did not take the node %s: %w", a.Change.Node, err) } return nil }