From 612962377893454b26d44fdc99e8616c7c94d955 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 22:09:28 +0200 Subject: [PATCH 1/2] Read settings for a config-form module that mesh-config holds, not only for one with a settings row (issue 1493) --- cmd/mesh-controller/compose_input.go | 39 +++++++++++++--- .../compose_input_settings_test.go | 45 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 cmd/mesh-controller/compose_input_settings_test.go diff --git a/cmd/mesh-controller/compose_input.go b/cmd/mesh-controller/compose_input.go index fc102b93..7e176e7b 100644 --- a/cmd/mesh-controller/compose_input.go +++ b/cmd/mesh-controller/compose_input.go @@ -375,7 +375,7 @@ func readLicences(ctx context.Context, open *stores) ([]compose.Licence, error) return out, nil } -// readSettings is each node's settings layers for every module that has any, read BEFORE any node is resolved: the first pass +// readSettings is each node's settings layers for every module that has any (a row, or config in mesh-config), read BEFORE any node is resolved: the first pass // resolves a node whose set takes a provider's served facts from the node that runs it, with that node's settings on them // (servedOnNode), so a mesh whose provider says ${setting:domain} resolves only with the settings in the Input. A module's // settings that fail to read are returned in unread, by node and module, and are an error only for a module a node resolves to @@ -385,13 +385,12 @@ func readSettings(ctx context.Context, open *stores, in *compose.Input) (unread if err != nil { return nil, err } - names := make([]string, 0, len(have)) - for module := range have { - if _, onShelf := in.Catalogue[module]; onShelf { - names = append(names, module) - } + names, err := modulesToRead(in.Catalogue, have, func(module string) (bool, error) { + return open.inventory.ReadsConfig(ctx, module) + }) + if err != nil { + return nil, err } - sort.Strings(names) for i := range in.Nodes { n := &in.Nodes[i] for _, module := range names { @@ -418,6 +417,32 @@ func readSettings(ctx context.Context, open *stores, in *compose.Input) (unread return unread, nil } +// modulesToRead is the modules on the shelf whose settings are read for a node: those with a settings row, and those in +// the config form whose values mesh-config holds (readsConfig), which have no row when they were never in the settings +// form (issue 1493: brother's scanner and printer, set only in mesh-config, composed empty). SettingsFor is where +// mesh-config's values are laid in, so a gate on the settings table alone leaves them out. +func modulesToRead(shelf map[string]catalogue.Manifest, have map[string]bool, readsConfig func(string) (bool, error)) ([]string, error) { + names := make([]string, 0, len(have)) + for module, m := range shelf { + if have[module] { + names = append(names, module) + continue + } + if !m.DeclaresConfig() { + continue + } + reads, err := readsConfig(module) + if err != nil { + return nil, err + } + if reads { + names = append(names, module) + } + } + sort.Strings(names) + return names, nil +} + // unreadSettings is the error of a module's settings that did not read, for a module the node resolves to. func unreadSettings(unread map[string]map[string]error, plans map[string]compose.Resolved) error { names := make([]string, 0, len(plans)) diff --git a/cmd/mesh-controller/compose_input_settings_test.go b/cmd/mesh-controller/compose_input_settings_test.go new file mode 100644 index 00000000..44fb9e10 --- /dev/null +++ b/cmd/mesh-controller/compose_input_settings_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "errors" + "reflect" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// configForm is a module in the config form, shaped as a real one (inventory's config_move_test). +func configForm(t *testing.T, name string) catalogue.Manifest { + t.Helper() + m, err := catalogue.ParseManifest([]byte(`{"module": "` + name + `", "version": "1", + "config": {"level": {"type": "string", "description": "how much it says", "default": "info"}}, + "resources": [{"id": "conf", "type": "file", "path": "/etc/` + name + `.json", "mode": "0640", + "content": "{\"level\": \"info\"}", "merge": "json", "takes-config": ["level"]}]}`)) + if err != nil { + t.Fatal(err) + } + return m +} + +// Issue 1493: a module in the config form whose values are set only in mesh-config has no settings row, and its +// settings are still read, because SettingsFor is where mesh-config's values are laid in. +func TestSettingsAreReadForAConfigModuleWithoutASettingsRow(t *testing.T) { + shelf := map[string]catalogue.Manifest{ + "brother": configForm(t, "brother"), // config only in mesh-config: no settings row + "older": {Module: "older", Version: "1"}, // settings form, with a row + "idle": {Module: "idle", Version: "1"}, // settings form, no row: nothing to read + "newer": configForm(t, "newer"), // config form, but the switch is off for it + } + have := map[string]bool{"older": true, "gone": true} // "gone" has a row and is not on the shelf + reads := func(module string) (bool, error) { return module == "brother", nil } + got, err := modulesToRead(shelf, have, reads) + if err != nil { + t.Fatal(err) + } + if want := []string{"brother", "older"}; !reflect.DeepEqual(got, want) { + t.Errorf("settings are read for %v, want %v", got, want) + } + if _, err := modulesToRead(shelf, have, func(string) (bool, error) { return false, errors.New("store down") }); err == nil { + t.Error("a failing ReadsConfig is not returned") + } +} From 7adadf756dfe19c034758e4da1509ea3afe40750 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 22:09:59 +0200 Subject: [PATCH 2/2] Test that a config module with no settings row is read from mesh-config (issue 1493) --- ...compose_input_settings_integration_test.go | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 cmd/mesh-controller/compose_input_settings_integration_test.go diff --git a/cmd/mesh-controller/compose_input_settings_integration_test.go b/cmd/mesh-controller/compose_input_settings_integration_test.go new file mode 100644 index 00000000..9e547627 --- /dev/null +++ b/cmd/mesh-controller/compose_input_settings_integration_test.go @@ -0,0 +1,45 @@ +//go:build integration + +package main + +import ( + "context" + "testing" +) + +// Issue 1493: a module in the config form whose value is set only in mesh-config, with no settings row, reaches the +// node's settings in the composition's input. +func TestAConfigModuleWithoutASettingsRowIsReadFromConfig(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + register(t, open, configForm(t, "brother")) + if _, err := assign(ctx, open, "anchor", "brother"); err != nil { + t.Fatal(err) + } + inv.ExecForTest(t, `insert into config_move (moved_by, why) values ('a test', 'issue 1493')`) + inv.ReadsConfigFrom(func(_ context.Context, node, module string) (map[string]any, error) { + if node == "anchor" && module == "brother" { + return map[string]any{"level": "debug"}, nil + } + return nil, nil + }) + in, _, err := readMeshAndNodes(ctx, open) + if err != nil { + t.Fatal(err) + } + if _, err := readSettings(ctx, open, &in); err != nil { + t.Fatal(err) + } + for _, n := range in.Nodes { + if n.Name != "anchor" { + continue + } + layers := n.Settings["brother"] + if len(layers) != 1 || layers[0].Values["level"] != "debug" { + t.Fatalf("anchor's settings for brother are %+v, want level debug from mesh-config", layers) + } + return + } + t.Fatal("no anchor node in the input") +}