From d6b867a87a19198985893c361aebcdefdebb14e1 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 11 Oct 2026 02:51:11 +0200 Subject: [PATCH] Restart what reads a secret family member when the member is given again (issue 405) secretsReadBy looked only at secrets declared by name, so a container mounting a member kept the value it started with. --- internal/catalogue/declaration.go | 22 ++++++++++--- internal/catalogue/secret_restart_test.go | 38 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index ff5be865..fb9010bc 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -1076,7 +1076,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string, // credential the mesh had replaced, because its manifest restarted it on its // environment file and nobody had thought to name the credential too. Composed here so // no manifest has to say it, for a container or a daemon that names the secret's path. - if reads := secretsReadBy(copied, m); len(reads) > 0 { + if reads := secretsReadBy(copied, m, with.Needed[m.Module]); len(reads) > 0 { copied["restart-on"] = withRestartOn(copied["restart-on"], reads) } // **A version prepares its state before it runs** (novox/hq ADR 0135). Derived from the @@ -2392,7 +2392,12 @@ func portOfEndpoint(values map[string]any, ports map[string]int) { // — named in its volumes, its environment or its env-files by the secret's placed path — as // restart-on ids. Nothing for other shapes, and nothing for a scheduled or run-once process, which // the host refuses a restart-on for (it runs again anyway, and reads the file afresh). -func secretsReadBy(resource map[string]any, m Manifest) []string { +// +// **A member of a secret family given here is read the same way** (novox/hq issue 405, ADR 0283 decision 6): +// it is an own secret placed at its own path, and a container that mounted it would keep the value it +// started with when the operator gives it again. given is the module's own secrets sealed to this +// machine; a member not given is no file, so nothing restarts on it. +func secretsReadBy(resource map[string]any, m Manifest, given map[string]string) []string { kind := fmt.Sprint(resource["type"]) if kind != "container" && kind != "process" { return nil @@ -2404,9 +2409,18 @@ func secretsReadBy(resource map[string]any, m Manifest) []string { for _, key := range []string{"volumes", "env", "env-file"} { mentioned = append(mentioned, stringsIn(resource[key])...) } + paths := map[string]string{} + for name, own := range m.OwnSecrets.Plain() { + paths[name] = own.Path + } + for name, sealed := range given { + if own, family, ok := m.OwnSecrets.Lookup(name); ok && family != "" && sealed != "" { + paths[name] = own.Path + } + } var out []string - for _, name := range sortedKeys(m.OwnSecrets.Plain()) { - path := m.OwnSecrets[name].Path + for _, name := range sortedKeys(paths) { + path := paths[name] if path == "" { continue } diff --git a/internal/catalogue/secret_restart_test.go b/internal/catalogue/secret_restart_test.go index 5460c096..560eb52d 100644 --- a/internal/catalogue/secret_restart_test.go +++ b/internal/catalogue/secret_restart_test.go @@ -50,3 +50,41 @@ func TestAContainerReadingAnOwnSecretIsRestartedWhenItChanges(t *testing.T) { t.Fatalf("a container that reads no secret was given one to restart on: %v", by["agent.other"]["restart-on"]) } } + +// A member of a secret family is an own secret too (novox/hq issue 405, ADR 0283 decision 6): a container that +// reads a member given is restarted when it changes, as for a secret declared by name. A member not given is no +// file, so nothing is restarted on it. +func TestAContainerReadingAFamilyMemberIsRestartedWhenItChanges(t *testing.T) { + m := Manifest{Module: "mounts", Version: "1", + OwnSecrets: OwnSecrets{"smb-password-*": {Path: "/var/lib/mesh/mounts/smb-password-*.secret", IssuedBy: IssuedOutside}}, + Resources: []map[string]any{ + {"id": "games", "type": "container", "name": "mounts-games", "network": "host", + "image": "registry.example/mounts@sha256:" + strings.Repeat("a", 64), + "volumes": []any{"/var/lib/mesh/mounts/smb-password-games.secret:/run/password:ro"}}, + {"id": "library", "type": "container", "name": "mounts-library", "network": "host", + "image": "registry.example/mounts@sha256:" + strings.Repeat("a", 64), + "volumes": []any{"/var/lib/mesh/mounts/smb-password-library.secret:/run/password:ro"}}, + }} + got, err := Resolve(shelf(m), []string{m.Module}, + Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{"container-runtime": true}}, World{}) + if err != nil { + t.Fatal(err) + } + out, err := got.Declaration(Rendering{Needed: map[string]map[string]string{"mounts": {"smb-password-games": "SEALED"}}}) + if err != nil { + t.Fatal(err) + } + by := map[string]map[string]any{} + for _, r := range out { + by[r["id"].(string)] = r + } + if want := []any{"mounts.needs-smb-password-games"}; !reflect.DeepEqual(by["mounts.games"]["restart-on"], want) { + t.Fatalf("a container reading a member given is not restarted on it: %v", by["mounts.games"]["restart-on"]) + } + if _, placed := by["mounts.needs-smb-password-games"]; !placed { + t.Fatalf("the member given is not placed, so a restart-on names nothing: %v", out) + } + if _, has := by["mounts.library"]["restart-on"]; has { + t.Fatalf("a container reading a member not given was given a restart-on naming nothing: %v", by["mounts.library"]["restart-on"]) + } +}