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