From db84142d38c6587366ca2080b26a773a3bff2741 Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 11:41:57 +0200 Subject: [PATCH] The controller's tools can set an assignment's settings Per-machine and mesh-wide settings could be set only from the controller's command line. The settings verb runs settings set|clear, passing the values inline, which the command now accepts as well as a file (novox/hq issue 198). --- cmd/mesh-controller/modules.go | 10 +++++++--- cmd/mesh-controller/seatverbs.go | 19 +++++++++++++++++++ cmd/mesh-controller/seatverbs_test.go | 16 ++++++++++++++++ internal/catalogue/verbs.go | 9 +++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index 9ad3ecb..6ca7a67 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -352,10 +352,14 @@ func settingsCommand(ctx context.Context, args []string) error { switch args[0] { case "set": if len(positionals) != 2 { - return errors.New("settings set [--node ]") + return errors.New("settings set [--node ]") } - raw, err := os.ReadFile(positionals[1]) - if err != nil { + // A file, or the values themselves when they begin with `{` — which is how the mesh's own + // `settings` tool passes them, having no file to hand over (novox/hq issue 198). + var raw []byte + if strings.HasPrefix(strings.TrimSpace(positionals[1]), "{") { + raw = []byte(positionals[1]) + } else if raw, err = os.ReadFile(positionals[1]); err != nil { return err } var values map[string]any diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index cca6f99..51640a5 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -129,6 +129,25 @@ func argvFor(verb string, args map[string]any) ([]string, error) { // Half of either shape: the command says its usage, which names both shapes, and that is // the answer the caller needs. return []string{"rotate"}, nil + case "settings": + // `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 { + return nil, err + } + argv := []string{"settings", "set", str("module")} + switch { + case str("clear") == "true": + argv = []string{"settings", "clear", str("module")} + case str("values") != "": + argv = append(argv, str("values")) + } + // Neither values nor clear: the command says its usage, which names both, and that is the + // answer the caller needs — the same as `rotate` given half of either shape. + if n := str("node"); n != "" { + argv = append(argv, "--node", n) + } + return argv, nil case "issue": // The same act as `module issue` at a shell (novox/hq design 25 §4): the account is minted // into the mesh's records and delivered at the machine's next push, which is the caller's to diff --git a/cmd/mesh-controller/seatverbs_test.go b/cmd/mesh-controller/seatverbs_test.go index e8b11fd..9bbc6b7 100644 --- a/cmd/mesh-controller/seatverbs_test.go +++ b/cmd/mesh-controller/seatverbs_test.go @@ -73,6 +73,22 @@ func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) { } } +// `settings` is `settings set|clear` at a shell, with the values passed inline (novox/hq issue 198). +func TestSettingsSetsOrClearsALayer(t *testing.T) { + argv, err := argvFor("settings", map[string]any{"module": "dnsmasq", "values": `{"a":1}`, "node": "ace"}) + if err != nil || strings.Join(argv, " ") != `settings set dnsmasq {"a":1} --node ace` { + t.Fatalf("set on a machine: %v %v", argv, err) + } + argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq", "clear": "true"}) + if strings.Join(argv, " ") != "settings clear dnsmasq" { + t.Fatalf("clear for the mesh: %v", argv) + } + argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq"}) + if strings.Join(argv, " ") != "settings set dnsmasq" { + t.Fatalf("a set with no values falls to the command's usage: %v", argv) + } +} + // `issue` is `module issue` at a shell: the module and the machine, and nothing that would push. A // module's bus account was mintable only from the controller's command line, so an agent working // through the tools could not finish a rollout that gave a module one (novox/hq issue 191). diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index 3a6e00e..61d77cb 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -136,6 +136,15 @@ var ControllerVerbs = []Verb{ "node": "the machine that runs the module", "module": "the module's name", }, []string{"node", "module"})}, + {Name: "settings", Description: "Set what an assignment is configured with: a module's settings for the whole mesh, " + + "or for one machine. Replaces that layer whole — what it does not name, it no longer sets — and takes effect " + + "at the next push. With clear, removes the layer and the module is back to what its definition says.", + Input: schema(map[string]string{ + "module": "the module's name", + "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", + }, []string{"module"})}, {Name: "build", Description: "Have the build machine build a repository. Answers at once with the build's id: " + "`builds` with that id follows it line by line, and the module is registered when the outcome comes.", Input: schema(map[string]string{ -- 2.54.0