Every method of builds, build requests, rollouts, merges and assignments reads and writes the chain alone: the else branches that read the tables, the movers and MoveChain with their tests, chain_moved reads, ChainReads and the flags that switched a kind, the store's assignments mirror (KeepAssignmentPlans needs none of it: it uses only the AssignmentMirrored type, mirrors.go, and AssignmentValues, provisioning.go) and its tick, the move code of the controller's start, and ForTest's chain_moved row. The listed store tests of the move go with their functions; the tests of the readers, requests, assignments and plans that lived beside them stay in chain_store_test.go. Tests that replayed old data migrations over the build, assignment and pin tables are removed with the tables. The static test that no plan is written but through SavePlansMoving now reads the plan's chain path.
193 lines
7.6 KiB
Go
193 lines
7.6 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// A value given by hand lives only until the module's first good start (novox/hq ADR 0228).
|
|
//
|
|
// **A person gives a module's own secret for one reason**: to adopt something already running that
|
|
// holds that value. A module the mesh installs fresh needs none — the mesh makes it. So for a secret
|
|
// the mesh may make (catalogue.OwnSecret.MeshMayMake: read at start, issued by nobody outside), a
|
|
// given value is kept until the module has started under the mesh on it, and then replaced with one
|
|
// the mesh makes, sealed and sent like any other. Nothing a person handled is left in force.
|
|
//
|
|
// What stays as given: a value an outside party issued (an API key, a bot token, a licence), which
|
|
// no value of the mesh's would replace; a value a module applies to a backend, until the staged
|
|
// rotation exists (ADR 0114); and a value given before this rule, which waits for a person to ask
|
|
// `secret rotate` — the mesh does not decide for them that what was given long ago is used nowhere
|
|
// else.
|
|
|
|
// AcceptGivenSecret is `secret accept` for a module's own secret: the value a person gave, sealed as
|
|
// AcceptSecretForModule seals it, and — for a secret the mesh may make — marked to be replaced after
|
|
// the module's first good start. It answers whether it was so marked.
|
|
//
|
|
// **Only the person's path marks.** The mesh's own code accepts values too — a bus account it
|
|
// issued, the controller's bus address — and those are the mesh's word to a broker, which a fresh
|
|
// random value would break; they go through AcceptSecretForModule and are never marked.
|
|
func (i *Inventory) AcceptGivenSecret(ctx context.Context, node, module, name, value string) (untilStart bool, err error) {
|
|
return i.acceptOwn(ctx, node, module, name, value, true)
|
|
}
|
|
|
|
// OwnSecretOrigin is where a module's own secret on a machine came from — OriginMade or
|
|
// OriginAccepted — and when it was made or given; empty for one it does not hold yet.
|
|
func (i *Inventory) OwnSecretOrigin(ctx context.Context, node, module, name string) (string, time.Time, error) {
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
var origin string
|
|
var at time.Time
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select origin, made_at from module_secret where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name).Scan(&origin, &at)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", time.Time{}, nil
|
|
}
|
|
return origin, at, err
|
|
}
|
|
|
|
// givenAgo is ", given <date>, N days ago" for a refusal about a value given to the mesh, and empty
|
|
// when the mesh holds none: how old an outside party's key is, said where a person learns it cannot
|
|
// be rotated by the mesh.
|
|
func (i *Inventory) givenAgo(ctx context.Context, nodeID any, module, name string) string {
|
|
var origin string
|
|
var at time.Time
|
|
err := i.store.Pool().QueryRow(ctx,
|
|
`select origin, made_at from module_secret where node = $1 and module = $2 and name = $3`,
|
|
nodeID, module, name).Scan(&origin, &at)
|
|
if err != nil || origin != OriginAccepted {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("; the value held was given %s, %d day(s) ago",
|
|
at.UTC().Format("2006-01-02"), int(time.Since(at).Hours()/24))
|
|
}
|
|
|
|
// GivenReplaced is one given value the mesh replaced after its module's first good start.
|
|
type GivenReplaced struct {
|
|
Module, Name string
|
|
// Given is when the replaced value was given.
|
|
Given time.Time
|
|
// Machines are the machines to send so the module, and every holder of a shared credential,
|
|
// starts again on the new value.
|
|
Machines []string
|
|
}
|
|
|
|
// ReplaceGivenAfterStart replaces every given value on a machine whose module has now started well
|
|
// under the mesh on it (novox/hq ADR 0228), and answers what it replaced; the caller sends the
|
|
// machines each names.
|
|
//
|
|
// **The signal is the machine's clean report of the declaration it was last sent** — every resource
|
|
// applied, nothing failed or refused — **when that declaration was sent after the value was given**,
|
|
// so it is the start on the given value that counts, not an older one. On an adopted machine the
|
|
// module must also be taken: until then the mesh runs nothing of it, and nothing has started under
|
|
// the mesh. Each row is claimed by clearing its mark before it is remade, so two reports arriving
|
|
// together replace a value once; a remake that fails puts the mark back, and the next good report
|
|
// tries again.
|
|
func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared string) ([]GivenReplaced, error) {
|
|
if declared == "" {
|
|
return nil, nil
|
|
}
|
|
// Only a module assigned there, from the assignments bucket.
|
|
assignedHere := map[string]bool{}
|
|
modules, err := i.assignedModules(ctx, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, m := range modules {
|
|
assignedHere[m] = true
|
|
}
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select s.node, s.module, s.name, s.replace_after_start
|
|
from module_secret s
|
|
join node n on n.id = s.node
|
|
where n.name = $1 and n.sent = $2 and n.sent_at > s.replace_after_start
|
|
and s.origin = 'accepted' and s.replace_after_start is not null
|
|
and (not n.adopted or exists (select 1 from taken t where t.node = s.node and t.module = s.module))
|
|
order by s.module, s.name`, node, declared)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
type due struct {
|
|
nodeID any
|
|
module, name string
|
|
given time.Time
|
|
}
|
|
var dues []due
|
|
for rows.Next() {
|
|
var d due
|
|
if err := rows.Scan(&d.nodeID, &d.module, &d.name, &d.given); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if !assignedHere[d.module] {
|
|
continue
|
|
}
|
|
dues = append(dues, d)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(dues) == 0 {
|
|
return nil, nil
|
|
}
|
|
key, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key == "" {
|
|
return nil, fmt.Errorf("%s has no sealing key, so the given values it holds cannot be replaced", node)
|
|
}
|
|
|
|
var out []GivenReplaced
|
|
var errs []error
|
|
for _, d := range dues {
|
|
claimed, err := i.store.Pool().Exec(ctx,
|
|
`update module_secret set replace_after_start = null
|
|
where node = $1 and module = $2 and name = $3 and origin = 'accepted'
|
|
and replace_after_start = $4`, d.nodeID, d.module, d.name, d.given)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
if claimed.RowsAffected() != 1 {
|
|
continue // replaced by another report, or given again since
|
|
}
|
|
m, err := i.declared(ctx, d.module)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
// The definition is asked again now, not only when the value was given: one that has since
|
|
// said the value is an outside party's, or applied, keeps it as given, and the mark stays gone.
|
|
if own, _, ok := m.OwnSecrets.Lookup(d.name); !ok || !own.MeshMayMake() {
|
|
continue
|
|
}
|
|
if err := i.remakeOwn(ctx, d.nodeID, key, m, d.module, d.name); err != nil {
|
|
if _, back := i.store.Pool().Exec(ctx,
|
|
`update module_secret set replace_after_start = $4
|
|
where node = $1 and module = $2 and name = $3 and origin = 'accepted'
|
|
and replace_after_start is null`, d.nodeID, d.module, d.name, d.given); back != nil {
|
|
err = errors.Join(err, fmt.Errorf("and its mark could not be put back: %w", back))
|
|
}
|
|
errs = append(errs, fmt.Errorf("%s's given %q on %s was not replaced: %w", d.module, d.name, node, err))
|
|
continue
|
|
}
|
|
machines, err := i.SharedHolders(ctx, node, d.module, d.name)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if len(machines) == 0 {
|
|
machines = []string{node}
|
|
}
|
|
out = append(out, GivenReplaced{Module: d.module, Name: d.name, Given: d.given, Machines: machines})
|
|
}
|
|
return out, errors.Join(errs...)
|
|
}
|