diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index 66804655..aeac0218 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -571,18 +571,14 @@ func settingsCommand(ctx context.Context, args []string) error { return err } m, ok := known[positionals[0]] - if !ok || len(m.Settings) == 0 { + if !ok || len(catalogue.PreferencesOf(m)) == 0 { return nil } var layers []catalogue.Layer if *node != "" { - if mesh, has, err := inv.Layer(ctx, "", positionals[0]); err != nil { + // Every source on the node: mesh config, node config, then the module's own (novox/hq issue 630). + if layers, err = inv.SettingsFor(ctx, *node, positionals[0]); err != nil { return err - } else if has { - layers = append(layers, catalogue.Layer{From: catalogue.MeshWideLayer, Values: mesh}) - } - if has { - layers = append(layers, catalogue.Layer{From: *node, Values: values}) } } else if has { layers = append(layers, catalogue.Layer{From: catalogue.MeshWideLayer, Values: values}) @@ -612,7 +608,7 @@ func settingsCommand(ctx context.Context, args []string) error { var listed []preferencesOf for _, e := range entries { m := e.Manifest - if len(m.Settings) == 0 || (only != "" && m.Module != only) { + if len(catalogue.PreferencesOf(m)) == 0 || (only != "" && m.Module != only) { continue } if *node != "" && !containsString(e.On, *node) { @@ -682,7 +678,11 @@ func describeEffective(module, where string, values []catalogue.SettingSource) s fmt.Fprintf(&b, "%s on %s, every value and where it comes from:\n", module, where) for _, v := range values { value, _ := json.Marshal(v.Value) - fmt.Fprintf(&b, " %s = %s (%s", v.Key, value, v.From) + from := v.From + if v.From == where && where != "the whole mesh" { + from = valueSource(module, where, v) + } + fmt.Fprintf(&b, " %s = %s (%s", v.Key, value, from) if v.HasDefault && !v.FromDefault { d, _ := json.Marshal(v.Default) fmt.Fprintf(&b, "; the default is %s", d) @@ -723,28 +723,27 @@ func describePreferences(modules []preferencesOf) string { on = "on " + strings.Join(p.Nodes, ", ") } fmt.Fprintf(&b, "%s (%s)\n", p.Manifest.Module, on) - keys := make([]string, 0, len(p.Manifest.Settings)) - for k := range p.Manifest.Settings { + declared := catalogue.PreferencesOf(p.Manifest) + keys := make([]string, 0, len(declared)) + for k := range declared { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - d := p.Manifest.Settings[k] + d := declared[k] def, _ := json.Marshal(d.Default) - fmt.Fprintf(&b, " %s, default %s: %s\n", k, def, d.Why) + falls := "" + if d.Predefined != "" { + falls = ", falls back to the predefined key " + d.Predefined + } + fmt.Fprintf(&b, " %s, default %s%s: %s\n", k, def, falls, d.Why) for _, n := range p.Nodes { for _, s := range p.On[n] { if s.Key != k { continue } v, _ := json.Marshal(s.Value) - from := s.From - if s.FromDefault { - from = catalogue.DefaultLayer - } else if s.From != catalogue.MeshWideLayer { - from = "the node" - } - fmt.Fprintf(&b, " %s: %s (%s)\n", n, v, from) + fmt.Fprintf(&b, " %s: %s (%s)\n", n, v, valueSource(p.Manifest.Module, n, s)) } } } @@ -752,6 +751,18 @@ func describePreferences(modules []preferencesOf) string { return b.String() } +// valueSource is where a value on a node comes from, as the four sources are said (novox/hq issue 630): +// "default", "mesh config", "node config ()", or " on " for the module's own value. +func valueSource(module, node string, s catalogue.SettingSource) string { + switch { + case s.FromDefault: + return catalogue.DefaultLayer + case s.From == catalogue.MeshConfigLayer, s.From == catalogue.NodeConfigLayer(node), s.From == catalogue.MeshWideLayer: + return s.From + } + return module + " on " + node +} + // nodeFlag is ` --node ` for a machine's layer, nothing for the whole mesh's. func nodeFlag(node string) string { if node == "" { diff --git a/cmd/mesh-controller/operator_wait.go b/cmd/mesh-controller/operator_wait.go index 48e8bae4..d006e501 100644 --- a/cmd/mesh-controller/operator_wait.go +++ b/cmd/mesh-controller/operator_wait.go @@ -88,7 +88,7 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er case w.Provision != "": return checkProvisionWait(module, machine, w, m, f) case w.Setting != "": - if _, declared := m.Settings[w.Setting]; !declared { + if !catalogue.DeclaresPreference(m, w.Setting) { return fmt.Errorf("says it waits for the setting %s, which %s does not declare", w.Setting, module) } return nil diff --git a/cmd/mesh-controller/probe_agent_root.go b/cmd/mesh-controller/probe_agent_root.go index 95111959..d9b0245b 100644 --- a/cmd/mesh-controller/probe_agent_root.go +++ b/cmd/mesh-controller/probe_agent_root.go @@ -211,7 +211,7 @@ func (r *rootReader) executeServed(ctx context.Context, machine string) (bool, s } holders++ var setting *any - if _, declared := e.Manifest.Settings[loginShellVerb]; declared { + if catalogue.DeclaresPreference(e.Manifest, loginShellVerb) { layers, err := r.settings(ctx, machine, e.Manifest.Module) if err != nil { served = true diff --git a/cmd/mesh-controller/proposals.go b/cmd/mesh-controller/proposals.go index 6dd4d000..5a5a1416 100644 --- a/cmd/mesh-controller/proposals.go +++ b/cmd/mesh-controller/proposals.go @@ -542,6 +542,11 @@ const ( // proposeCommand is `settings propose`, on this controller's stores and bus. func proposeCommand(ctx context.Context, module, node, valuesArg string, clear, replace bool) error { + if node == "" && !clear { + // There is no module layer for the whole mesh (novox/hq issue 630); clearing one left over still works. + return fmt.Errorf("%s: there is no module layer for the whole mesh — an assignment is one module on one "+ + "node (novox/hq issue 630). Propose it for one node with --node. Nothing was proposed", module) + } var values map[string]any if !clear { if valuesArg == "" { diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index ff43179d..3660438d 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -881,6 +881,48 @@ func (a *verbArguments) commandLine() ([]string, error) { argv = append(argv, "--adopted") } return argv, nil + case "config": + // Config values from four sources (novox/hq issue 630): mesh config and node config for a predefined + // key, and a module's own value on one node. A listing without key; a set or clear says why. + key := str("key") + if str("module") != "" && str("node") == "" && (key != "" || on("clear")) { + return nil, errors.New("config: there is no module layer for the whole mesh — an assignment is one module " + + "on one node (novox/hq issue 630); give node, or leave module out to set mesh config for a predefined key") + } + place := func(argv []string) []string { + if n := str("node"); n != "" { + argv = append(argv, "--node", n) + } + if m := str("module"); m != "" { + argv = append(argv, "--module", m) + } + return argv + } + switch { + case key == "": + if str("value") != "" || on("clear") || on("history") || str("why") != "" { + return nil, errors.New("config: a set, a clear or a history names its key") + } + return place([]string{"config", "list"}), nil + case on("history"): + if str("value") != "" || on("clear") || str("module") != "" || str("why") != "" { + return nil, errors.New("config: history reads a key's mesh or node config, and takes only node") + } + return place([]string{"config", "history", key}), nil + case on("clear") && str("value") != "": + return nil, errors.New("config: a value or clear, not both") + case on("clear"): + if err := need("why"); err != nil { + return nil, err + } + return append(place([]string{"config", "clear", key}), "--why", str("why")), nil + case str("value") != "": + if err := need("why"); err != nil { + return nil, err + } + return append(place([]string{"config", "set", key, str("value")}), "--why", str("why")), nil + } + return nil, errors.New("config: with key, give value, clear or history") case "settings": // Every module's preferences, their defaults and each machine's value (novox/hq ADR 0262): // the one interface for them, so no module builds a settings tool of its own. Asked for by @@ -1614,6 +1656,7 @@ var commandReadForms = map[string]func(rest []string) bool{ "node": func(r []string) bool { return subIn(r, "list", "show") }, "module": func(r []string) bool { return subIn(r, "list") }, "settings": func(r []string) bool { return subIn(r, "show", "preferences", "proposals") }, + "config": func(r []string) bool { return flagsOnly(r) || subIn(r, "list", "history") }, "retire": func(r []string) bool { return subIn(r, "list") }, "cleanup": func(r []string) bool { return subIn(r, "list") }, "delivery": func(r []string) bool { return subIn(r, "plan", "walks") },