Land feature 57's controller work on its release branch
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).
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// The move of the controller's secrets into the vault (novox/hq ADR 0349 clause 7; design 57's V3, issue 676; the
|
||||
// operator's decision of 2026-10-14 on that issue).
|
||||
//
|
||||
// The controller opens none of the values it holds. Each row is moved by the node-engine of a machine it was sealed
|
||||
// to: a module's own secret by its machine's, a pair credential by either end's. This is what the controller reads to
|
||||
// ask each machine — every row sealed to it, as the store holds it — and where it keeps what moved. Nothing here
|
||||
// opens, makes or changes a value: the rows are read as they are.
|
||||
|
||||
// SecretToMove is one row of the controller's store as the move asks a machine to hand it in.
|
||||
type SecretToMove struct {
|
||||
// Secret is the vault's name for the row (VaultNameOfOwn, VaultNameOfPair).
|
||||
Secret string
|
||||
// Node is the machine asked: the module's machine for an own secret, the end being asked for a pair.
|
||||
Node string
|
||||
// Module and Name are an own secret's; empty for a pair.
|
||||
Module, Name string
|
||||
// Provider, Provision, Consumer, ConsumerModule and Local are a pair credential's; empty for an own secret.
|
||||
Provider, Provision, Consumer, ConsumerModule, Local string
|
||||
Origin string
|
||||
// Generation is the controller's stamp of the act that made the value (ADR 0158, ADR 0305 §5); empty for none.
|
||||
Generation string
|
||||
MadeAt time.Time
|
||||
// Sealed is the value sealed to Node, and SealedTo the key the row says it was sealed to.
|
||||
Sealed, SealedTo string
|
||||
// Current says SealedTo is Node's sealing key now: only then can its engine open it.
|
||||
Current bool
|
||||
// Digest is the digest of the row's sealed text as the store holds it — both ends' for a pair, so either end
|
||||
// reads the same — by which a row the controller changed after it moved is told from one that did not.
|
||||
Digest string
|
||||
// Moved is the record of the row having moved, by either end, or nil.
|
||||
Moved *SecretMoved
|
||||
}
|
||||
|
||||
// Own says the row is a module's own secret.
|
||||
func (s SecretToMove) Own() bool { return s.Module != "" }
|
||||
|
||||
// SealedDigest is sha256 of a row's sealed texts, each on its own line, in hex: nothing of the value is kept.
|
||||
func SealedDigest(sealed ...string) string {
|
||||
h := sha256.New()
|
||||
for _, s := range sealed {
|
||||
h.Write([]byte(s + "\n"))
|
||||
}
|
||||
return "sha256:" + hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// SecretMoved is a row the vault took: which machine's engine handed it in, the vault's generation, the digest of the
|
||||
// sealed text it was moved from, and when.
|
||||
type SecretMoved struct {
|
||||
Node string
|
||||
Generation int
|
||||
Digest string
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// VaultNameOfOwn is the vault's name for a module's own secret (mesh-vault OwnSecret).
|
||||
func VaultNameOfOwn(node, module, name string) string {
|
||||
return "own/" + node + "/" + module + "/" + name
|
||||
}
|
||||
|
||||
// VaultNameOfPair is the vault's name for one row of the pair credentials (mesh-vault PairSecret): both ends, the
|
||||
// consuming module, and the local name last when it has one.
|
||||
func VaultNameOfPair(provider, provision, consumer, consumerModule, local string) string {
|
||||
name := "pair/" + provider + "/" + provision + "/" + consumer + "/" + consumerModule
|
||||
if local != "" {
|
||||
name += "/" + local
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// SecretsToMoveFrom is every row sealed to a machine, as the store holds it: its modules' own secrets, and each pair
|
||||
// credential it is an end of, sealed to it; each with whether its key is the machine's now and whether it moved.
|
||||
func (i *Inventory) SecretsToMoveFrom(ctx context.Context, node string) ([]SecretToMove, error) {
|
||||
record, err := i.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current, err := i.SealingKeyOf(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moved, err := i.secretsMoved(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []SecretToMove
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select module, name, origin, coalesce(generation, ''), made_at, sealed, node_key from module_secret
|
||||
where node = $1 order by module, name`, record.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
s := SecretToMove{Node: node}
|
||||
if err := rows.Scan(&s.Module, &s.Name, &s.Origin, &s.Generation, &s.MadeAt, &s.Sealed, &s.SealedTo); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
s.Secret, s.Digest = VaultNameOfOwn(node, s.Module, s.Name), SealedDigest(s.Sealed)
|
||||
out = append(out, s)
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err = i.store.Pool().Query(ctx,
|
||||
`select p.name, s.name, c.name, s.consumer_module, s.local, s.origin, coalesce(s.generation, ''), s.created_at,
|
||||
case when s.consumer = $1 then s.for_consumer else s.for_provider end,
|
||||
case when s.consumer = $1 then s.consumer_key else s.provider_key end,
|
||||
s.for_consumer, s.for_provider
|
||||
from secret s join node p on p.id = s.provider join node c on c.id = s.consumer
|
||||
where s.consumer = $1 or s.provider = $1
|
||||
order by p.name, s.name, c.name, s.consumer_module, s.local`, record.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
s := SecretToMove{Node: node}
|
||||
var forConsumer, forProvider string
|
||||
if err := rows.Scan(&s.Provider, &s.Provision, &s.Consumer, &s.ConsumerModule, &s.Local, &s.Origin,
|
||||
&s.Generation, &s.MadeAt, &s.Sealed, &s.SealedTo, &forConsumer, &forProvider); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
s.Digest = SealedDigest(forConsumer, forProvider)
|
||||
s.Secret = VaultNameOfPair(s.Provider, s.Provision, s.Consumer, s.ConsumerModule, s.Local)
|
||||
out = append(out, s)
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for n := range out {
|
||||
out[n].Current = current != "" && out[n].SealedTo == current
|
||||
if m, ok := moved[out[n].Secret]; ok {
|
||||
out[n].Moved = &m
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (i *Inventory) secretsMoved(ctx context.Context) (map[string]SecretMoved, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select m.secret, n.name, m.generation, m.sealed_digest, m.moved_at from secret_moved m join node n on n.id = m.node`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]SecretMoved{}
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var m SecretMoved
|
||||
if err := rows.Scan(&name, &m.Node, &m.Generation, &m.Digest, &m.At); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[name] = m
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RecordSecretMoved keeps that the vault took a row, read back the same, from a machine's engine. Kept once: the
|
||||
// first record of a row stands, so the other end of a pair moving it again changes nothing.
|
||||
func (i *Inventory) RecordSecretMoved(ctx context.Context, s SecretToMove, generation int) error {
|
||||
record, err := i.NodeByName(ctx, s.Node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`insert into secret_moved (secret, node, generation, sealed_digest) values ($1, $2, $3, $4)
|
||||
on conflict (secret) do nothing`, s.Secret, record.ID, generation, s.Digest)
|
||||
return err
|
||||
}
|
||||
|
||||
// SecretMove is the move as started: when, by whom, and when the record of asks was copied.
|
||||
type SecretMove struct {
|
||||
StartedAt time.Time
|
||||
StartedBy string
|
||||
AsksMovedAt *time.Time
|
||||
}
|
||||
|
||||
// StartSecretMove records that the move started, the first time; later starts keep the first. It answers the move
|
||||
// as recorded.
|
||||
func (i *Inventory) StartSecretMove(ctx context.Context, by string) (SecretMove, error) {
|
||||
if _, err := i.store.Pool().Exec(ctx,
|
||||
`insert into secret_move (started_by) values ($1) on conflict (one) do nothing`, by); err != nil {
|
||||
return SecretMove{}, err
|
||||
}
|
||||
m, _, err := i.SecretMoveStarted(ctx)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// SecretMoveStarted is the move as recorded, and whether it was started: a machine heard from is moved only then.
|
||||
func (i *Inventory) SecretMoveStarted(ctx context.Context) (SecretMove, bool, error) {
|
||||
var m SecretMove
|
||||
err := i.store.Pool().QueryRow(ctx, `select started_at, started_by, asks_moved_at from secret_move`).
|
||||
Scan(&m.StartedAt, &m.StartedBy, &m.AsksMovedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return SecretMove{}, false, nil
|
||||
}
|
||||
return m, err == nil, err
|
||||
}
|
||||
|
||||
// RecordAsksMoved keeps that the record of asks was copied into the vault.
|
||||
func (i *Inventory) RecordAsksMoved(ctx context.Context) error {
|
||||
_, err := i.store.Pool().Exec(ctx, `update secret_move set asks_moved_at = now() where asks_moved_at is null`)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user