A tools bundle is given its words, composed per machine; what they name is the account's to read (hq ADR 0192)

build.artifacts[].env on a bundle: words and values written with ${dir:…} and ${port:…} only,
refused when a value carries any other reference (a secret's content, a binding) or names a word
the runtime sets for itself, and on any artifact that is not a bundle. Resolved per machine like a
container's environment and handed to the runtime as MESH_TOOL_ENV, module by module, in the unit
so a change restarts it. Every file and directory of the module a word names, or that holds one, is
owned by the account the runtime runs as where it says no owner, since a tool reads as that account.
This commit is contained in:
jochen
2026-10-03 15:32:03 +02:00
parent 62650cd48c
commit f27e31954f
5 changed files with 278 additions and 1 deletions
+116
View File
@@ -1,6 +1,7 @@
package catalogue
import (
"encoding/json"
"fmt"
"strings"
"testing"
@@ -210,3 +211,118 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
}
})
}
// novox/hq ADR 0192: a tools bundle says what it is given; the composer resolves it per machine as
// a container's environment, hands it to the runtime as the module's words, and makes what the
// words name readable by the account the runtime runs as.
func TestABundleIsGivenItsWordsResolvedForThisMachine(t *testing.T) {
with := Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{
RuntimeModule: {"broker": "sealed-credential"},
"dash": {"token": "sealed-token"},
}}
dash := Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
Listens: []Listening{{Name: "web", Port: 3000, Protocol: "tcp"}},
OwnSecrets: OwnSecrets{"token": {Path: "${dir:mesh-state}/token"}},
Resources: []map[string]any{
{"id": "mesh-state", "type": "directory", "mode": "0700", "place": "mesh"},
{"id": "config", "type": "file", "path": "${dir:mesh-state}/config.json", "mode": "0600", "content": "{}\n"},
{"id": "unrelated", "type": "file", "path": "/etc/dash.conf", "content": "x\n"},
},
Build: &Build{Artifacts: []Artifact{{Name: "tools", Kind: ArtifactBundle, Language: "typescript",
Entrypoints: []string{"tools/index.js"},
Env: map[string]string{
"DASH_CONFIG_FILE": "${dir:mesh-state}/config.json",
"DASH_TOKEN_FILE": "${dir:mesh-state}/token",
"DASH_URL": "http://127.0.0.1:${port:3000}",
"DASH_ADMIN": "mesh-admin",
}}}}}
if problems := dash.Build.problems(dash.Module); len(problems) > 0 {
t.Fatalf("a bundle's words written with ${dir:…} and ${port:…} were refused: %v", problems)
}
dash, err := dash.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle,
Reference: ArtifactStoreScheme + "dash/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
other := aToolsModule(t, "nftables", "tools/index.js")
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{dash, other, theRuntime(t)}}.Declaration(with)
if err != nil {
t.Fatal(err)
}
dir := fileNamed(out, "dash.mesh-state")
if dir == nil {
t.Fatalf("no directory: %v", ids(out))
}
at := fmt.Sprint(dir["path"])
process := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())
env := process["env"].(map[string]string)
var given map[string]map[string]string
if err := json.Unmarshal([]byte(env[RuntimeToolEnv]), &given); err != nil {
t.Fatalf("the runtime's %s is not JSON: %q", RuntimeToolEnv, env[RuntimeToolEnv])
}
want := map[string]string{
"DASH_CONFIG_FILE": at + "/config.json",
"DASH_TOKEN_FILE": at + "/token",
"DASH_URL": "http://127.0.0.1:3000",
"DASH_ADMIN": "mesh-admin",
}
if fmt.Sprint(given["dash"]) != fmt.Sprint(want) {
t.Errorf("dash is given %v, want %v", given["dash"], want)
}
if _, has := given["nftables"]; has {
t.Errorf("a module that declares no words was given some: %v", given)
}
// What the words name is the account's to read; nothing else of the module's is touched.
for _, id := range []string{"dash.mesh-state", "dash.config", "dash." + NeedID("token")} {
if r := fileNamed(out, id); r == nil || r["owner"] != "ops" {
t.Errorf("%s is not the account's to read: %v", id, r)
}
}
if r := fileNamed(out, "dash.unrelated"); r == nil || r["owner"] != nil {
t.Errorf("a file no word names was given an owner: %v", r)
}
t.Run("on a machine with no account the runtime is root and nothing is re-owned", func(t *testing.T) {
out, err := Resolution{Node: "anchor", Modules: []Manifest{dash, theRuntime(t)}}.Declaration(with)
if err != nil {
t.Fatal(err)
}
if r := fileNamed(out, "dash.config"); r["owner"] != nil {
t.Errorf("re-owned with no account: %v", r)
}
})
t.Run("a change to a module's words changes the runtime's process", func(t *testing.T) {
changed := dash
changed.Bundles = append([]Bundle(nil), dash.Bundles...)
changed.Bundles[0].Env = map[string]string{"DASH_ADMIN": "somebody-else"}
out2, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{changed, theRuntime(t)}}.Declaration(with)
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(fileNamed(out2, RuntimeModule+"."+RuntimeProcessID())["env"]) == fmt.Sprint(env) {
t.Error("the runtime's process is the same after a module's words changed, so it would not restart")
}
})
}
func TestABundlesWordsAreRefusedWhenTheyAreNotPathsOrConstants(t *testing.T) {
m := Manifest{Module: "dash", Version: "1", Build: &Build{Artifacts: []Artifact{
{Name: "tools", Kind: ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"},
Env: map[string]string{"DASH_TOKEN": "${secret:token}", RuntimeBrokerFile: "/x", "DASH_PEER": "${bound:db:url}"}},
{Name: "runtime", Kind: ArtifactImage, From: "Dockerfile", Env: map[string]string{"X": "y"}},
}}}
said := strings.Join(m.Build.problems(m.Module), "\n")
for _, want := range []string{
`"tools" gives DASH_TOKEN the value "${secret:token}"`,
`"tools" gives DASH_PEER the value "${bound:db:url}"`,
`"tools" gives itself ` + RuntimeBrokerFile,
`"runtime" is a "image" and says what it is given`,
} {
if !strings.Contains(said, want) {
t.Errorf("not refused: %s\nsaid:\n%s", want, said)
}
}
}