ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
91 lines
4.7 KiB
Go
91 lines
4.7 KiB
Go
package resolution
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// What reads one of a module's own secrets is restarted when the secret changes (novox/hq issue 203,
|
|
// issue 206): the build machine kept an hour-old credential open because its manifest restarted it
|
|
// on its environment file alone. Composed, so a manifest need not say it; a scheduled process is
|
|
// left alone, because the host refuses a restart-on for one and it reads the file afresh each run.
|
|
func TestAContainerReadingAnOwnSecretIsRestartedWhenItChanges(t *testing.T) {
|
|
m := mcat.Manifest{Module: "agent", Version: "1",
|
|
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/agent/broker"}},
|
|
Resources: []map[string]any{
|
|
{"id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/agent", "mode": "0700"},
|
|
{"id": "settings", "type": "file", "path": "/var/lib/mesh/agent/agent.env", "mode": "0600", "content": "A=1\n"},
|
|
{"id": "server", "type": "container", "name": "agent", "network": "host",
|
|
"image": "registry.example/agent@sha256:" + strings.Repeat("a", 64),
|
|
"volumes": []any{"/var/lib/mesh/agent:/run/mesh:ro", "/var/lib/mesh/agent/broker:/run/mesh/broker:ro"},
|
|
"env-file": []any{"/var/lib/mesh/agent/agent.env"},
|
|
"restart-on": []any{"settings"}},
|
|
{"id": "nightly", "type": "container", "name": "agent-nightly", "schedule": "0 3 * * *",
|
|
"image": "registry.example/agent@sha256:" + strings.Repeat("a", 64),
|
|
"volumes": []any{"/var/lib/mesh/agent/broker:/run/mesh/broker:ro"}},
|
|
{"id": "other", "type": "container", "name": "agent-other",
|
|
"image": "registry.example/agent@sha256:" + strings.Repeat("a", 64)},
|
|
}}
|
|
got, err := Resolve(shelf(m), []string{m.Module}, mcat.Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{"container-runtime": true}}, mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Declaration(got, mcat.Rendering{Needed: map[string]map[string]string{"agent": {"broker": "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{"agent.settings", "agent.needs-broker"}; !reflect.DeepEqual(by["agent.server"]["restart-on"], want) {
|
|
t.Fatalf("the server reads the credential and is not restarted on it: %v", by["agent.server"]["restart-on"])
|
|
}
|
|
if _, has := by["agent.nightly"]["restart-on"]; has {
|
|
t.Fatalf("a scheduled container was given a restart-on, which the host refuses: %v", by["agent.nightly"]["restart-on"])
|
|
}
|
|
if _, has := by["agent.other"]["restart-on"]; has {
|
|
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 := mcat.Manifest{Module: "mounts", Version: "1",
|
|
OwnSecrets: mcat.OwnSecrets{"smb-password-*": {Path: "/var/lib/mesh/mounts/smb-password-*.secret", IssuedBy: mcat.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}, mcat.Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{"container-runtime": true}}, mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Declaration(got, mcat.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"])
|
|
}
|
|
}
|