373 lines
13 KiB
Go
373 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// **`settings` answers by forwarding to `config`** (novox/hq ADR 0348 §1, design 57 K5, issue 664). Once the move of
|
|
// config has run, a module in the config form keeps its config values in mesh-config (inventory.ReadsConfig), so the
|
|
// settings verb reads and writes them there: `show` lists what `config list` lists, `set` sets each value as the
|
|
// module's own on the node and `clear` clears them, and `propose` proposes each through mesh-config. Placement stays
|
|
// the controller's and is read and written beside the assignment as before (ADR 0348 §11). A module still in the
|
|
// settings form, and every module before the move, keeps today's verb unchanged.
|
|
|
|
// configAsk asks mesh-config's verb in the controller's own name, so mesh-config takes a value as the controller
|
|
// carries it (ADR 0348 §8); a test replaces it.
|
|
var configAsk = func(ctx context.Context, args map[string]any) (json.RawMessage, error) {
|
|
var result json.RawMessage
|
|
err := onTheBus(func(conn *nats.Conn) error {
|
|
answer, err := link.AskMeshSeatToolAs(ctx, conn, broker.ControllerName, "mesh-config", "config", args, 30*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if answer.Error != "" {
|
|
return errors.New(answer.Error)
|
|
}
|
|
result = answer.Result
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
// configListed is what `config list` answers, as the settings verb reads it.
|
|
type configListed struct {
|
|
Resolved []resolvedEntry `json:"resolved"`
|
|
Set []struct {
|
|
Level configLevel `json:"level"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
By string `json:"by"`
|
|
} `json:"set"`
|
|
}
|
|
|
|
func listConfig(ctx context.Context, module, node string) (configListed, error) {
|
|
args := map[string]any{"action": "list", "module": module}
|
|
if node != "" {
|
|
args["node"] = node
|
|
}
|
|
raw, err := configAsk(ctx, args)
|
|
if err != nil {
|
|
return configListed{}, fmt.Errorf("mesh-config does not answer %s's config values: %w", module, err)
|
|
}
|
|
var l configListed
|
|
if err := json.Unmarshal(raw, &l); err != nil {
|
|
return configListed{}, fmt.Errorf("mesh-config's listing cannot be read: %w", err)
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// ownValues is the module's own values on the node, as mesh-config holds them; none for the whole mesh, which has no
|
|
// module value (ADR 0348 §3).
|
|
func (l configListed) ownValues(module, node string) map[string]any {
|
|
out := map[string]any{}
|
|
if node == "" {
|
|
return out
|
|
}
|
|
for _, s := range l.Set {
|
|
if s.Level.Level == "module" && s.Level.Node == node && s.Level.Module == module {
|
|
var v any
|
|
if json.Unmarshal(s.Value, &v) == nil {
|
|
out[s.Key] = v
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// forwardSettings answers a settings action for a module whose config values are mesh-config's, and says whether it
|
|
// did: false leaves the action to today's verb.
|
|
func forwardSettings(ctx context.Context, inv *inventory.Inventory, action, module, node string, positionals []string,
|
|
replace bool) (bool, error) {
|
|
if module == "" {
|
|
return false, nil
|
|
}
|
|
reads, err := inv.ReadsConfig(ctx, module)
|
|
if err != nil || !reads {
|
|
return false, err
|
|
}
|
|
where := whereOf(node)
|
|
switch action {
|
|
case "show":
|
|
listed, err := listConfig(ctx, module, node)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
layer, _, err := inv.Layer(ctx, node, module)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
if layer == nil {
|
|
layer = map[string]any{}
|
|
}
|
|
for k, v := range listed.ownValues(module, node) {
|
|
layer[k] = v
|
|
}
|
|
if len(layer) == 0 {
|
|
fmt.Printf("%s on %s: no layer — the module's definition says\n", module, where)
|
|
} else {
|
|
shown, err := json.MarshalIndent(layer, "", " ")
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
fmt.Println(string(shown))
|
|
}
|
|
fmt.Printf(" its config values are mesh-config's since the move of config (issue 664): mesh-config.config\n")
|
|
fmt.Printf("%s on %s, every value and where it comes from:\n", module, where)
|
|
for _, r := range listed.Resolved {
|
|
value := string(r.Value)
|
|
if value == "" {
|
|
value = "unset"
|
|
}
|
|
on := ""
|
|
if node == "" {
|
|
on = " on " + r.Node
|
|
}
|
|
fmt.Printf(" %s%s = %s (%s)\n", r.Key, on, value, r.Source)
|
|
}
|
|
return true, nil
|
|
|
|
case "set":
|
|
var raw []byte
|
|
if strings.HasPrefix(strings.TrimSpace(positionals[1]), "{") {
|
|
raw = []byte(positionals[1])
|
|
} else {
|
|
return true, errors.New("the settings of a module whose config values are mesh-config's are given as " +
|
|
"{…}, never a file; or set each with mesh-config.config")
|
|
}
|
|
var values map[string]any
|
|
if err := json.Unmarshal(raw, &values); err != nil {
|
|
return true, fmt.Errorf("%s is not a settings object: %w", positionals[1], err)
|
|
}
|
|
return true, forwardSet(ctx, inv, module, node, values, replace)
|
|
|
|
case "clear":
|
|
listed, err := listConfig(ctx, module, node)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
own := listed.ownValues(module, node)
|
|
layer, _, err := inv.Layer(ctx, node, module)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
before := map[string]any{}
|
|
for k, v := range layer {
|
|
before[k] = v
|
|
}
|
|
for k, v := range own {
|
|
before[k] = v
|
|
}
|
|
if err := refuseTerminalSettingsThroughAVerb(ctx, inv, before, nil, module, where); err != nil {
|
|
return true, err
|
|
}
|
|
if err := refuseTrustedThroughAVerb(ctx, inv, module, where, movedKeys(own)); err != nil {
|
|
return true, err
|
|
}
|
|
for _, k := range movedKeys(own) {
|
|
if _, err := configAsk(ctx, map[string]any{"action": "clear", "module": module, "node": node, "key": k,
|
|
"why": "settings clear " + module + " on " + where, "by": setByWords()}); err != nil {
|
|
return true, fmt.Errorf("%s of %s on %s was not cleared: %w", k, module, where, err)
|
|
}
|
|
}
|
|
if len(layer) > 0 {
|
|
if err := inv.ClearSettingsBy(ctx, node, module, setByWords()); err != nil {
|
|
return true, err
|
|
}
|
|
}
|
|
fmt.Printf("%s on %s is back to what the module says\n", module, where)
|
|
return true, nil
|
|
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// forwardSet sets a layer of a module whose config values are mesh-config's: its placement beside the assignment
|
|
// as before, each config value as the module's own on the node, and each it no longer names cleared.
|
|
func forwardSet(ctx context.Context, inv *inventory.Inventory, module, node string, values map[string]any,
|
|
replace bool) error {
|
|
where := whereOf(node)
|
|
config, placement := catalogue.SplitPlacement(values)
|
|
listed, err := listConfig(ctx, module, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
own := listed.ownValues(module, node)
|
|
layer, _, err := inv.Layer(ctx, node, module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
before := map[string]any{}
|
|
for k, v := range layer {
|
|
before[k] = v
|
|
}
|
|
for k, v := range own {
|
|
before[k] = v
|
|
}
|
|
if err := refuseTerminalSettingsThroughAVerb(ctx, inv, before, values, module, where); err != nil {
|
|
return err
|
|
}
|
|
added, changed, removed := settingsChange(before, values)
|
|
if len(removed) > 0 && !replace {
|
|
return fmt.Errorf("%s on %s: this layer would no longer set %s. A layer is replaced whole; read it with "+
|
|
"`settings show %s%s` and include what should stay, or add --replace if the removal is meant (novox/hq "+
|
|
"ADR 0217). Nothing was changed", module, where, strings.Join(removed, ", "), module, nodeFlag(node))
|
|
}
|
|
var toSet, toClear []string
|
|
for _, k := range movedKeys(config) {
|
|
if was, had := own[k]; !had || !sameJSON(valueJSON(was), config[k]) {
|
|
toSet = append(toSet, k)
|
|
}
|
|
}
|
|
for _, k := range movedKeys(own) {
|
|
if _, kept := config[k]; !kept {
|
|
toClear = append(toClear, k)
|
|
}
|
|
}
|
|
if node == "" && len(toSet)+len(toClear) > 0 {
|
|
return fmt.Errorf("%s has no own value for the whole mesh (novox/hq ADR 0348 §3): name the node, or set mesh "+
|
|
"config for the predefined key a value falls back to with mesh-config.config. Nothing was changed", module)
|
|
}
|
|
if err := refuseTrustedThroughAVerb(ctx, inv, module, where, append(append([]string{}, toSet...), toClear...)); err != nil {
|
|
return err
|
|
}
|
|
if placement == nil {
|
|
placement = map[string]any{}
|
|
}
|
|
if _, placed := catalogue.SplitPlacement(layer); !sameLayer(placed, placement) {
|
|
if err := inv.SetSettingsBy(ctx, node, module, placement, setByWords()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, k := range toSet {
|
|
if _, err := configAsk(ctx, map[string]any{"action": "set", "module": module, "node": node, "key": k,
|
|
"value": config[k], "why": "settings set " + module + " on " + where, "by": setByWords()}); err != nil {
|
|
return fmt.Errorf("%s of %s on %s was not set: %w", k, module, where, err)
|
|
}
|
|
}
|
|
for _, k := range toClear {
|
|
if _, err := configAsk(ctx, map[string]any{"action": "clear", "module": module, "node": node, "key": k,
|
|
"why": "settings set " + module + " on " + where + " --replace", "by": setByWords()}); err != nil {
|
|
return fmt.Errorf("%s of %s on %s was not cleared: %w", k, module, where, err)
|
|
}
|
|
}
|
|
fmt.Printf("%s on %s:\n", module, where)
|
|
if len(added)+len(changed)+len(removed) == 0 {
|
|
fmt.Println(" nothing changed")
|
|
}
|
|
for _, p := range added {
|
|
fmt.Printf(" + %s\n", p)
|
|
}
|
|
for _, p := range changed {
|
|
fmt.Printf(" ~ %s\n", p)
|
|
}
|
|
for _, p := range removed {
|
|
fmt.Printf(" - %s\n", p)
|
|
}
|
|
fmt.Println(" its config values are mesh-config's (issue 664): each was set there, with its history; the module " +
|
|
"takes them when its files are filled again")
|
|
return nil
|
|
}
|
|
|
|
func valueJSON(v any) json.RawMessage {
|
|
raw, _ := json.Marshal(v)
|
|
return raw
|
|
}
|
|
|
|
// refuseTrustedThroughAVerb refuses, through a verb, a trusted config value (ADR 0348 §8): mesh-config takes the
|
|
// controller's word for one as given at its terminal, and whoever may call a verb includes agents.
|
|
func refuseTrustedThroughAVerb(ctx context.Context, inv *inventory.Inventory, module, where string, keys []string) error {
|
|
verb, through := throughAVerb()
|
|
if !through || len(keys) == 0 {
|
|
return nil
|
|
}
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("which config values of %s are trusted cannot be read, so nothing was changed: %w", module, err)
|
|
}
|
|
for _, k := range keys {
|
|
if shelf[module].Settings[k].Trusted {
|
|
return fmt.Errorf("%s of %s on %s is trusted (novox/hq ADR 0348 §8): it is set at the controller's terminal "+
|
|
"or on the operator's Approve, never through a verb alone (this line came through %q). Propose it with "+
|
|
"mesh-config.config, action propose. Nothing was changed", k, module, where, verb)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// forwardPropose proposes, through mesh-config, each config value of a layer proposed for a module whose config
|
|
// values are mesh-config's (ADR 0348 §8): mesh-config asks the operator under its own name, one ask per value. A
|
|
// proposal naming placement too is refused, so the operator is never asked half of it; placement alone is proposed
|
|
// as before. Says whether it answered.
|
|
func forwardPropose(ctx context.Context, inv *inventory.Inventory, module, node, valuesArg string, clear bool) (bool, error) {
|
|
reads, err := inv.ReadsConfig(ctx, module)
|
|
if err != nil || !reads {
|
|
return false, err
|
|
}
|
|
where := whereOf(node)
|
|
if node == "" {
|
|
return true, fmt.Errorf("%s has no own value for the whole mesh (novox/hq ADR 0348 §3): name the node, or "+
|
|
"propose mesh config for the predefined key a value falls back to with mesh-config.config. Nothing was "+
|
|
"proposed", module)
|
|
}
|
|
proposals := map[string]any{}
|
|
var clears []string
|
|
if clear {
|
|
listed, err := listConfig(ctx, module, node)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
layer, _, err := inv.Layer(ctx, node, module)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
if len(layer) > 0 {
|
|
return true, fmt.Errorf("%s on %s keeps placement beside its assignment (%s): propose clearing it apart "+
|
|
"from its config values. Nothing was proposed", module, where, strings.Join(movedKeys(layer), ", "))
|
|
}
|
|
clears = movedKeys(listed.ownValues(module, node))
|
|
} else {
|
|
var values map[string]any
|
|
if err := json.Unmarshal([]byte(valuesArg), &values); err != nil {
|
|
return true, fmt.Errorf("%s is not a settings object: %w", valuesArg, err)
|
|
}
|
|
config, placement := catalogue.SplitPlacement(values)
|
|
if len(placement) > 0 {
|
|
if len(config) > 0 {
|
|
return true, fmt.Errorf("%s names placement (%s) and config values: propose them apart, the config "+
|
|
"values through mesh-config. Nothing was proposed", module, strings.Join(movedKeys(placement), ", "))
|
|
}
|
|
return false, nil
|
|
}
|
|
proposals = config
|
|
}
|
|
for _, k := range movedKeys(proposals) {
|
|
answer, err := configAsk(ctx, map[string]any{"action": "propose", "module": module, "node": node, "key": k,
|
|
"value": proposals[k], "why": "settings propose " + module + " on " + where})
|
|
if err != nil {
|
|
return true, fmt.Errorf("%s of %s on %s was not proposed: %w", k, module, where, err)
|
|
}
|
|
fmt.Printf("%s of %s on %s, proposed through mesh-config: %s\n", k, module, where, answer)
|
|
}
|
|
for _, k := range clears {
|
|
answer, err := configAsk(ctx, map[string]any{"action": "propose", "module": module, "node": node, "key": k,
|
|
"clear": true, "why": "settings propose " + module + " on " + where + " --clear"})
|
|
if err != nil {
|
|
return true, fmt.Errorf("clearing %s of %s on %s was not proposed: %w", k, module, where, err)
|
|
}
|
|
fmt.Printf("clearing %s of %s on %s, proposed through mesh-config: %s\n", k, module, where, answer)
|
|
}
|
|
return true, nil
|
|
}
|