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).
218 lines
8.6 KiB
Go
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)
|
|
}
|