From bc289cbe04d411df270b5132580fe3d9f2b2dd69 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 17 Sep 2026 23:35:12 +0200 Subject: [PATCH] The restart-on rename reads both list shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A resource composed in code carries restart-on as []string; the rename only read []any, so the overlay's registry-trust reload kept its bare reference, pointed at nothing, and the runtime was never restarted — the trust was on disk and not in the daemon, with every check passing. Diagnosed on the built-store-cross-node bed, run 8 (issues 042/048). --- internal/catalogue/declaration.go | 41 ++++++++++++++++----- internal/catalogue/reflects_renamed_test.go | 32 ++++++++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 internal/catalogue/reflects_renamed_test.go diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 2204a62..76339dc 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -484,16 +484,7 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) { // the daemon's. Written `.`, and a dot is what marks it as already // answered: prefixing it again would point at nothing, silently, and the daemon would // serve the old names for ever while everything reported success. - if reflects, ok := resource["restart-on"].([]any); ok { - var renamed []any - for _, id := range reflects { - named := fmt.Sprint(id) - if strings.Contains(named, ".") { - renamed = append(renamed, named) - continue - } - renamed = append(renamed, m.Module+"."+named) - } + if renamed := reflectsRenamed(m.Module, resource["restart-on"]); renamed != nil { copied["restart-on"] = renamed } out = append(out, copied) @@ -566,6 +557,36 @@ type Contribution struct { // Named after both. Named after the machine alone, two modules on one node wrote to one path: the // second overwrote the first, and the provisioner — reading a directory — saw one consumer where // there were two. +// reflectsRenamed is a resource's restart-on list under the module's prefix, or nil when it has +// none. It reads both the shape JSON parsing produces ([]any) and the shape code composing +// resources natively produces ([]string): a reference that was skipped because its list arrived +// in the other shape would point at nothing — silently, with the service never restarting and +// every check passing, which is how a runtime kept serving without the registry trust its +// daemon file already carried. +func reflectsRenamed(module string, reflects any) []any { + var names []string + switch v := reflects.(type) { + case []any: + for _, id := range v { + names = append(names, fmt.Sprint(id)) + } + case []string: + names = v + } + if names == nil { + return nil + } + var renamed []any + for _, named := range names { + if strings.Contains(named, ".") { + renamed = append(renamed, named) + continue + } + renamed = append(renamed, module+"."+named) + } + return renamed +} + func grantPath(directory, consumer, module string) string { return strings.TrimRight(directory, "/") + "/" + consumer + "." + module + ".secret" } diff --git a/internal/catalogue/reflects_renamed_test.go b/internal/catalogue/reflects_renamed_test.go new file mode 100644 index 0000000..e5bee0b --- /dev/null +++ b/internal/catalogue/reflects_renamed_test.go @@ -0,0 +1,32 @@ +package catalogue + +import ( + "reflect" + "testing" +) + +func TestReflectsRenamedReadsBothShapes(t *testing.T) { + // The list arrives as []any when the resource was parsed from JSON, and as []string when it + // was composed in code — the overlay's registry trust is the second kind. A shape that was + // skipped would leave the reference unprefixed, pointing at nothing, and the service would + // never restart while every check passed (novox/hq issues 042/048, the run-8 diagnosis). + parsed := reflectsRenamed("mesh-wireguard", []any{"registry-trust"}) + if !reflect.DeepEqual(parsed, []any{"mesh-wireguard.registry-trust"}) { + t.Fatalf("parsed shape: %v", parsed) + } + composed := reflectsRenamed("mesh-wireguard", []string{"registry-trust"}) + if !reflect.DeepEqual(composed, []any{"mesh-wireguard.registry-trust"}) { + t.Fatalf("composed shape: %v", composed) + } + + // A name already under a module keeps it: that is how a service reflects a file another + // module put on the machine. + kept := reflectsRenamed("resolver", []string{"mesh-wireguard.fact-node-names", "own-config"}) + if !reflect.DeepEqual(kept, []any{"mesh-wireguard.fact-node-names", "resolver.own-config"}) { + t.Fatalf("dotted name was not kept: %v", kept) + } + + if got := reflectsRenamed("any", nil); got != nil { + t.Fatalf("no list should rename to nothing, got %v", got) + } +}