Files
mesh-controller/internal/inventory/config_move.go
T

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)
}