The controller plans config, templates and moved providers' provisions, hands bus accounts to the vault and judges the new manifest words, on the feature's SDK and node-engine validator (feature 57, config and provisioning).
130 lines
5.3 KiB
Go
130 lines
5.3 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// The move of one value into the vault, asked of a machine's node-engine (design 57's V3, issue 676; the operator's
|
|
// decision of 2026-10-14 on that issue).
|
|
//
|
|
// The controller cannot open a value it holds; the machine it was sealed to can. So the controller asks that
|
|
// machine's engine on its own subject, one row of its store at a time, signed as a hand-over is under a context of
|
|
// its own: the row, where its value came from, the controller's generation, when it was made, and the value as the
|
|
// store holds it, sealed to that machine. The engine opens it, hands it to the vault's move-in sealed to the call's
|
|
// key, and answers what the vault kept once the vault's read-back opens to the same value. The engine holds the
|
|
// same shapes in its own link code (mesh-host internal/link MoveSecretAsk, MoveSecretAnswer); a test on each side
|
|
// holds the field names.
|
|
|
|
// MoveSecretContext is prefixed to a move ask's bytes before signing: never another ask's, nor a declaration's.
|
|
const MoveSecretContext = "novox-mesh move-secret v1\n"
|
|
|
|
// MoveSecretAsk is a move's own fields, beside a hand-over's: module and name for a module's own secret on the
|
|
// machine asked; provider, provision, consumer, consuming module and local name for a pair credential.
|
|
type MoveSecretAsk struct {
|
|
Module string `json:"module,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Provider string `json:"provider,omitempty"`
|
|
Provision string `json:"provision,omitempty"`
|
|
Consumer string `json:"consumer,omitempty"`
|
|
ConsumerModule string `json:"consumer_module,omitempty"`
|
|
Local string `json:"local,omitempty"`
|
|
Origin string `json:"origin"`
|
|
Generation string `json:"generation,omitempty"`
|
|
MadeAt time.Time `json:"made_at"`
|
|
Sealed string `json:"sealed"`
|
|
SealedTo string `json:"sealed_to,omitempty"`
|
|
}
|
|
|
|
// MoveSecretAnswer is the engine's answer: what the vault kept, read back the same, or why nothing was moved.
|
|
type MoveSecretAnswer struct {
|
|
Said string `json:"said,omitempty"`
|
|
Refused string `json:"refused,omitempty"`
|
|
Secret string `json:"secret,omitempty"`
|
|
Generation int `json:"generation,omitempty"`
|
|
Unchanged bool `json:"unchanged,omitempty"`
|
|
}
|
|
|
|
// MoveSecretWithin is how long the controller waits for one move: the engine asks the vault twice.
|
|
const MoveSecretWithin = 45 * time.Second
|
|
|
|
// AskMoveSecret asks one machine's node-engine to move one row's value into the vault, and reads its answer. An
|
|
// error is the ask not reaching an engine, or an answer that is not one; a refusal is the engine's, in the answer.
|
|
func AskMoveSecret(ctx context.Context, conn *nats.Conn, signer Signer, node, by string, row MoveSecretAsk,
|
|
timeout time.Duration) (MoveSecretAnswer, error) {
|
|
if conn == nil {
|
|
return MoveSecretAnswer{}, errors.New("this controller is not on the bus")
|
|
}
|
|
if signer == nil {
|
|
return MoveSecretAnswer{}, errors.New("no signing key, so nothing can be asked of an engine")
|
|
}
|
|
nonce := make([]byte, 16)
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return MoveSecretAnswer{}, err
|
|
}
|
|
raw, err := json.Marshal(struct {
|
|
HandOverAsk
|
|
MoveSecretAsk
|
|
}{HandOverAsk{Node: node, By: by, Expires: time.Now().UTC().Add(HandOverGood), Nonce: hex.EncodeToString(nonce)},
|
|
row})
|
|
if err != nil {
|
|
return MoveSecretAnswer{}, err
|
|
}
|
|
signature, err := signer.Sign(ctx, append([]byte(MoveSecretContext), raw...))
|
|
if err != nil {
|
|
return MoveSecretAnswer{}, fmt.Errorf("cannot sign the ask: %w", err)
|
|
}
|
|
body, err := json.Marshal(SignedHandOver{Ask: raw, Signature: signature})
|
|
if err != nil {
|
|
return MoveSecretAnswer{}, err
|
|
}
|
|
asking, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
refused, stop := refusalsOf(conn, broker.AskMoveSecretSubject(node))
|
|
defer stop()
|
|
type replied struct {
|
|
msg *nats.Msg
|
|
err error
|
|
}
|
|
done := make(chan replied, 1)
|
|
go func() {
|
|
msg, err := conn.RequestWithContext(asking, broker.AskMoveSecretSubject(node), body)
|
|
done <- replied{msg, err}
|
|
}()
|
|
var reply *nats.Msg
|
|
select {
|
|
case r := <-done:
|
|
reply, err = r.msg, r.err
|
|
case why := <-refused:
|
|
cancel()
|
|
return MoveSecretAnswer{}, fmt.Errorf("the bus refused the controller asking %s to move a value: %v", node, why)
|
|
}
|
|
switch {
|
|
case errors.Is(err, nats.ErrNoResponders):
|
|
return MoveSecretAnswer{}, fmt.Errorf("nothing on %s answers a move: its node-engine is not running, is not on "+
|
|
"the bus, or is older than this ask (issue 676); nothing was moved", node)
|
|
case errors.Is(err, context.DeadlineExceeded), errors.Is(err, nats.ErrTimeout):
|
|
return MoveSecretAnswer{}, fmt.Errorf("%s did not answer a move within %s; whether the vault took it is not "+
|
|
"known — moving it again changes nothing if it did", node, timeout)
|
|
case err != nil:
|
|
return MoveSecretAnswer{}, err
|
|
}
|
|
var answer MoveSecretAnswer
|
|
if err := json.Unmarshal(reply.Data, &answer); err != nil {
|
|
return MoveSecretAnswer{}, fmt.Errorf("%s answered a move with something unreadable: %w", node, err)
|
|
}
|
|
if (answer.Refused == "") == (answer.Secret == "") {
|
|
return MoveSecretAnswer{}, fmt.Errorf("%s answered a move with neither what the vault kept nor a refusal", node)
|
|
}
|
|
return answer, nil
|
|
}
|