Merge pull request 'A provider waiting for the operator holds its consumers, and a wait beside another condition is said (issue 405)' (#216) from fix/405-a-waiting-provider-holds-its-consumers into main

This commit was merged in pull request #216.
This commit is contained in:
2026-10-11 01:23:35 +00:00
6 changed files with 685 additions and 61 deletions
+18 -4
View File
@@ -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
}
+38
View File
@@ -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"])
}
}