List every module's preferences in the settings verb (hq ADR 0262)
One verb is the interface to every preference, so no module builds a settings tool of its own: each key, its default and why, and every assigned machine's value with its source.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
@@ -396,7 +397,8 @@ func assignCommand(ctx context.Context, verb string, args []string) error {
|
||||
func settingsCommand(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("settings show <module> [--node <node>] [--history], settings set <module> <file> " +
|
||||
"[--node <node>] [--replace], or settings clear <module> [--node <node>]")
|
||||
"[--node <node>] [--replace], settings clear <module> [--node <node>], or settings preferences " +
|
||||
"[<module>] [--node <node>]")
|
||||
}
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
@@ -536,6 +538,46 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
fmt.Print(describeEffective(positionals[0], where, catalogue.Effective(m, layers)))
|
||||
return nil
|
||||
|
||||
case "preferences":
|
||||
// Every module's preferences, and each machine's value with its source (novox/hq ADR 0262).
|
||||
if len(positionals) > 1 {
|
||||
return errors.New("settings preferences [<module>] [--node <node>]")
|
||||
}
|
||||
only := ""
|
||||
if len(positionals) == 1 {
|
||||
only = positionals[0]
|
||||
}
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var listed []preferencesOf
|
||||
for _, e := range entries {
|
||||
m := e.Manifest
|
||||
if len(m.Settings) == 0 || (only != "" && m.Module != only) {
|
||||
continue
|
||||
}
|
||||
p := preferencesOf{Manifest: m, On: map[string][]catalogue.SettingSource{}}
|
||||
for _, n := range e.On {
|
||||
if *node != "" && n != *node {
|
||||
continue
|
||||
}
|
||||
p.Nodes = append(p.Nodes, n)
|
||||
layers, err := inv.SettingsFor(ctx, n, m.Module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.On[n] = catalogue.Effective(m, layers)
|
||||
}
|
||||
listed = append(listed, p)
|
||||
}
|
||||
if only != "" && len(listed) == 0 {
|
||||
fmt.Printf("%s declares no preferences\n", only)
|
||||
return nil
|
||||
}
|
||||
fmt.Print(describePreferences(listed))
|
||||
return nil
|
||||
|
||||
case "clear":
|
||||
if len(positionals) != 1 {
|
||||
return errors.New("settings clear <module> [--node <node>]")
|
||||
@@ -547,7 +589,7 @@ func settingsCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("settings has no %q; it has show, set and clear", args[0])
|
||||
return fmt.Errorf("settings has no %q; it has show, set, clear and preferences", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +610,58 @@ func describeEffective(module, where string, values []catalogue.SettingSource) s
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// preferencesOf is one module's preferences and its value on each machine it is assigned to.
|
||||
type preferencesOf struct {
|
||||
Manifest catalogue.Manifest
|
||||
Nodes []string
|
||||
On map[string][]catalogue.SettingSource
|
||||
}
|
||||
|
||||
// describePreferences lists each module's preferences — key, default and why — and, per machine it is
|
||||
// assigned to, the value and where it comes from (novox/hq ADR 0262).
|
||||
func describePreferences(modules []preferencesOf) string {
|
||||
if len(modules) == 0 {
|
||||
return "no module declares a preference\n"
|
||||
}
|
||||
var b strings.Builder
|
||||
for i, p := range modules {
|
||||
if i > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
on := "assigned nowhere"
|
||||
if len(p.Nodes) > 0 {
|
||||
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 {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
d := p.Manifest.Settings[k]
|
||||
def, _ := json.Marshal(d.Default)
|
||||
fmt.Fprintf(&b, " %s, default %s: %s\n", k, def, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// nodeFlag is ` --node <node>` for a machine's layer, nothing for the whole mesh's.
|
||||
func nodeFlag(node string) string {
|
||||
if node == "" {
|
||||
|
||||
@@ -761,6 +761,22 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
}
|
||||
return argv, nil
|
||||
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
|
||||
// name, or by naming no module, since a layer is always some module's.
|
||||
if list := str("list"); list != "" || str("module") == "" {
|
||||
if list != "" && list != "preferences" {
|
||||
return nil, fmt.Errorf("settings lists %q only; %q is not a listing", "preferences", list)
|
||||
}
|
||||
argv := []string{"settings", "preferences"}
|
||||
if m := str("module"); m != "" {
|
||||
argv = append(argv, m)
|
||||
}
|
||||
if n := str("node"); n != "" {
|
||||
argv = append(argv, "--node", n)
|
||||
}
|
||||
return argv, nil
|
||||
}
|
||||
// `settings set|clear` at a shell (novox/hq issue 198). The values travel as an argument
|
||||
// because a tool has no file to hand the command; the command reads either.
|
||||
if err := need("module"); err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
@@ -19,3 +20,52 @@ func TestSettingsSayWhereEachValueComesFrom(t *testing.T) {
|
||||
t.Fatalf("said\n%s\nwant\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// `settings` with list "preferences" is the one listing of every module's preferences; module and node
|
||||
// narrow it, and no other listing is taken.
|
||||
func TestSettingsListPreferences(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
args map[string]any
|
||||
want string
|
||||
}{
|
||||
{map[string]any{"list": "preferences"}, "settings preferences"},
|
||||
{map[string]any{"list": "preferences", "module": "dunst"}, "settings preferences dunst"},
|
||||
{map[string]any{"list": "preferences", "node": "laptop"}, "settings preferences --node laptop"},
|
||||
} {
|
||||
argv, err := argvFor("settings", c.args)
|
||||
if err != nil || strings.Join(argv, " ") != c.want {
|
||||
t.Errorf("%v: %v %v, want %s", c.args, argv, err, c.want)
|
||||
}
|
||||
}
|
||||
if _, err := argvFor("settings", map[string]any{"list": "everything"}); err == nil {
|
||||
t.Error("a listing other than preferences was taken")
|
||||
}
|
||||
if argv, err := argvFor("settings", map[string]any{}); err != nil || strings.Join(argv, " ") != "settings preferences" {
|
||||
t.Errorf("settings naming no module is the listing: %v %v", argv, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreferencesSayEachMachinesValueAndItsSource(t *testing.T) {
|
||||
m := catalogue.Manifest{Module: "dunst", Settings: map[string]catalogue.SettingDeclaration{
|
||||
"font-size": {Kind: catalogue.KindPreference, Default: float64(10), Why: "readable at 100 DPI"},
|
||||
"width": {Kind: catalogue.KindPreference, Default: float64(250), Why: "forty characters"},
|
||||
}}
|
||||
on := map[string][]catalogue.SettingSource{
|
||||
"laptop": catalogue.Effective(m, []catalogue.Layer{{From: "laptop", Values: map[string]any{"font-size": float64(16)}}}),
|
||||
"desk": catalogue.Effective(m, []catalogue.Layer{{From: catalogue.MeshWideLayer, Values: map[string]any{"width": float64(300)}}}),
|
||||
}
|
||||
got := describePreferences([]preferencesOf{{Manifest: m, Nodes: []string{"desk", "laptop"}, On: on}})
|
||||
want := "dunst (on desk, laptop)\n" +
|
||||
" font-size, default 10: readable at 100 DPI\n" +
|
||||
" desk: 10 (default)\n" +
|
||||
" laptop: 16 (the node)\n" +
|
||||
" width, default 250: forty characters\n" +
|
||||
" desk: 300 (the mesh)\n" +
|
||||
" laptop: 250 (default)\n"
|
||||
if got != want {
|
||||
t.Fatalf("said\n%s\nwant\n%s", got, want)
|
||||
}
|
||||
if describePreferences(nil) != "no module declares a preference\n" {
|
||||
t.Fatal("an empty listing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,19 +251,22 @@ var ControllerVerbs = []Verb{
|
||||
{Name: "settings", Description: "Read or set what an assignment is configured with: a module's settings for the whole " +
|
||||
"mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it — " +
|
||||
"then every value the module gives a default or a layer sets, with where it comes from: the module's default, " +
|
||||
"the mesh, or the machine (novox/hq ADR 0262); " +
|
||||
"the mesh, or the machine (novox/hq ADR 0262); with list \"preferences\", or with no module, every " +
|
||||
"module's preferences and each machine's value; " +
|
||||
"with history, the layers it replaced. Setting replaces that layer whole and answers each key it adds (+), " +
|
||||
"changes (~) and removes (-); a set that would remove a key is refused unless replace says it is meant " +
|
||||
"(novox/hq ADR 0217). Takes effect at the next push. With clear, removes the layer and the module is back to " +
|
||||
"what its definition says; a cleared or replaced layer is kept in the history.",
|
||||
Input: schema(map[string]string{
|
||||
"module": "the module's name",
|
||||
"module": "the module's name; with list, only that module's",
|
||||
"values": "the settings as a JSON object, for set",
|
||||
"node": "one machine; the whole mesh when absent",
|
||||
"clear": "\"true\" to remove the layer instead of setting it; not with values",
|
||||
"replace": "\"true\": with values, the set is meant to remove the keys the layer had and it does not name",
|
||||
"history": "\"true\": without values or clear, the layers this one replaced, the latest first",
|
||||
}, []string{"module"}, "clear", "replace", "history")},
|
||||
"list": "\"preferences\": every module's preferences — key, default and why — and the value on each " +
|
||||
"machine it is assigned to with where it comes from; module and node narrow it (novox/hq ADR 0262)",
|
||||
}, nil, "clear", "replace", "history")},
|
||||
{Name: "command", Description: "Run one command line of the controller's own, as you would type it at its " +
|
||||
"shell — `node account g14 jochen`, `node show ace`, `module list` — and answer what it printed. The " +
|
||||
"generic verb beside the named ones (novox/hq ADR 0154): everything the binary can do, without a verb " +
|
||||
|
||||
Reference in New Issue
Block a user