Files
mesh-controller/internal/inventory/config_move.go
T
mesh-tools 97a2d841c7 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).
2026-10-15 21:27:33 +00:00

218 lines
8.6 KiB
Go

package inventory
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/catalogue"
)
// **The settings layers move into mesh-config** (novox/hq ADR 0348 §9 and its consequences, design 57 K5, issue
// 664). Each config value a settings layer holds is set in mesh-config by the move of config (`move-config`), one of
// the moves of data a release runs once after its delivery; then the controller reads a module's config values from
// mesh-config_resolved where it read its layers (ADR 0349 clause 17's old way, an exception to ADR 0348 §9 that
// design 57's R1 removes).
//
// **The switch** is the row of config_move (migration 0102), written only by the move once every value it set reads
// back through mesh-config. While it is absent, composition reads the layers exactly as before. Once it is there, a
// module reads mesh-config when it is in the config form and none of its layers still holds a config value: a
// module still in the settings form (the media catalogue's, until they move) keeps its layers, and one that moved
// to the config form after the move keeps its layer until the move runs again, so no value is ever read from where
// it is not yet.
// ConfigValues reads what mesh-config resolved for one module on one node: each value it takes from mesh config,
// node config or its own value on that node, keyed by its config key. A value it takes from its manifest's default,
// or a required value nothing sets, is not among them: composition has the manifest's defaults already, and a
// taken key's default is never laid over its file (the lead's decision on issue 664).
type ConfigValues func(ctx context.Context, node, module string) (map[string]any, error)
// ReadsConfigFrom gives the inventory where it reads config values once the switch is on. Nil reads none, and a
// module the switch reaches then fails to compose by name, never silently without its values.
func (i *Inventory) ReadsConfigFrom(read ConfigValues) { i.configValues = read }
// ConfigMove is the switch: when the move of config ran, who ran it, and why.
type ConfigMove struct {
Moved bool
At string
By string
Why string
}
// ConfigMoved says whether the move of config has run: the switch.
func (i *Inventory) ConfigMoved(ctx context.Context) (ConfigMove, error) {
var m ConfigMove
err := i.store.Pool().QueryRow(ctx,
`select to_char(moved_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), coalesce(moved_by, ''), why
from config_move`).Scan(&m.At, &m.By, &m.Why)
if errors.Is(err, pgx.ErrNoRows) {
return ConfigMove{}, nil
}
if err != nil {
return ConfigMove{}, err
}
m.Moved = true
return m, nil
}
// ReadsConfig says whether a module's config values are read from mesh-config rather than its layers: the move of
// config has run, the module is registered in the config form, and none of its layers still holds a config value.
func (i *Inventory) ReadsConfig(ctx context.Context, module string) (bool, error) {
var moved, holds bool
var raw []byte
err := i.store.Pool().QueryRow(ctx,
`select exists (select 1 from config_move),
(select manifest from module where name = $1),
exists (select 1 from settings where module = $1 and values <> '{}'::jsonb)`, module).
Scan(&moved, &raw, &holds)
if err != nil {
return false, err
}
if !moved || holds || raw == nil {
return false, nil
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
return false, err
}
return m.DeclaresConfig(), nil
}
// fromConfig lays a module's config values, as mesh-config resolved them for the node, over the layers read for
// it: the node's layer takes them as its values, and holds them alone when the module has no layer of the node's.
// The layers keep their placement; their values were emptied by the move.
func (i *Inventory) fromConfig(ctx context.Context, nodeName, module string, layers []catalogue.Layer) (
[]catalogue.Layer, error) {
if i.configValues == nil {
return nil, fmt.Errorf("%s on %s takes its config values from mesh-config, and this controller has no way to "+
"read them (novox/hq ADR 0348 §9, issue 664); nothing is composed without them", module, nodeName)
}
values, err := i.configValues(ctx, nodeName, module)
if err != nil {
return nil, fmt.Errorf("%s on %s takes its config values from mesh-config, and they cannot be read: %w",
module, nodeName, err)
}
for k := range layers {
// A layer the move emptied keeps its placement alone: its config values are mesh-config's now.
layers[k].Values = map[string]any{}
}
if len(values) == 0 {
return layers, nil
}
for k := range layers {
if layers[k].From == nodeName {
layers[k].Values = values
return layers, nil
}
}
return append(layers, catalogue.Layer{From: nodeName, Values: values}), nil
}
// LayerToMove is one settings layer as the move of config reads it: whose, its values, and when it was set.
type LayerToMove struct {
// Node is the layer's machine, empty for the whole mesh's.
Node string `json:"node"`
Module string `json:"module"`
Values map[string]any `json:"values"`
}
// LayersToMove is every settings layer that holds a value, the whole mesh's first, then by machine and module, and
// whether any still holds a placement key (the move of placement has not run).
func (i *Inventory) LayersToMove(ctx context.Context) ([]LayerToMove, []string, error) {
rows, err := i.store.Pool().Query(ctx,
`select coalesce(n.name, ''), s.module, s.values
from settings s left join node n on n.id = s.node
where s.values <> '{}'::jsonb
order by s.node is not null, n.name, s.module`)
if err != nil {
return nil, nil, err
}
defer rows.Close()
var out []LayerToMove
placed := map[string]bool{}
for rows.Next() {
var l LayerToMove
var raw []byte
if err := rows.Scan(&l.Node, &l.Module, &raw); err != nil {
return nil, nil, err
}
if err := json.Unmarshal(raw, &l.Values); err != nil {
return nil, nil, err
}
if _, placement := catalogue.SplitPlacement(l.Values); len(placement) > 0 {
where := l.Node
if where == "" {
where = "the whole mesh"
}
placed[l.Module+" on "+where] = true
}
out = append(out, l)
}
if err := rows.Err(); err != nil {
return nil, nil, err
}
holding := make([]string, 0, len(placed))
for k := range placed {
holding = append(holding, k)
}
sort.Strings(holding)
return out, holding, nil
}
// configMovedWhy is why a layer the move of config moved went into the history.
const configMovedWhy = "moved to mesh-config, ADR 0348 §9 (issue 664)"
// FinishConfigMove ends the move of config once every value it set reads back through mesh-config: in one
// transaction, each layer moved is kept whole in the history first (why: moved to mesh-config), its values are
// emptied (its row stays, so the placement beside it stays), and the switch is written with who ran it and why.
// A layer that changed since the move read it refuses the whole, and nothing is changed: the move is asked again.
func (i *Inventory) FinishConfigMove(ctx context.Context, moved []LayerToMove, by, why string) error {
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
for _, l := range moved {
raw, err := json.Marshal(l.Values)
if err != nil {
return err
}
// The layer's row: its machine's, or the whole mesh's (no machine named, so no node).
const theLayer = `s.module = $1 and s.node is not distinct from (select id from node where name = $2)`
var still bool
if err := tx.QueryRow(ctx,
`select exists (select 1 from settings s where `+theLayer+` and s.values = $3::jsonb for update of s)`,
l.Module, l.Node, string(raw)).Scan(&still); err != nil {
return err
}
if !still {
where := l.Node
if where == "" {
where = "the whole mesh"
}
return fmt.Errorf("the layer of %s on %s changed since the move read it; nothing was changed: ask the "+
"move again", l.Module, where)
}
if _, err := tx.Exec(ctx,
`insert into settings_history (node, module, values, set_at, replaced_by, set_by, why)
select s.node, s.module, `+layerWhole+`, s.set_at, 'moved', s.set_by, $3
from settings s where `+theLayer, l.Module, l.Node, configMovedWhy); err != nil {
return err
}
if _, err := tx.Exec(ctx, `update settings s set values = '{}'::jsonb where `+theLayer, l.Module, l.Node); err != nil {
return err
}
}
if _, err := tx.Exec(ctx,
`insert into config_move (moved_by, why) values ($1, $2)
on conflict (only_one) do update set moved_at = now(), moved_by = excluded.moved_by, why = excluded.why`,
nullable(by), why); err != nil {
return err
}
return tx.Commit(ctx)
}