package catalogue import ( "encoding/json" "fmt" "strings" "testing" ) // The node's tool runtime (novox/hq ADR 0175, to-be 38): where the runtime module is assigned, a // machine is sent every assigned module's tools bundle as an archive, and the runtime's own process // loading them. Where it is not, the machine is sent exactly what it was sent before. var bundleDigest = "sha256:" + strings.Repeat("b", 64) // aToolsModule is a module whose tools come as a compiled bundle and nothing else — the shape every // module takes once its tool container goes (to-be 38 WP4). func aToolsModule(t *testing.T, name string, entrypoints ...string) Manifest { t.Helper() m := Manifest{Module: name, Version: "1", Tools: []string{"status"}, Build: &Build{Artifacts: []Artifact{ {Name: "tools", Kind: ArtifactBundle, Language: "typescript", Entrypoints: entrypoints}, }}} resolved, err := m.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle, Reference: ArtifactStoreScheme + name + "/tools/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } return resolved } // theRuntime is the runtime module as the catalogue holds it: its own bundle, run rather than // loaded, and its broker secret to receive the node's credential in. func theRuntime(t *testing.T) Manifest { t.Helper() m := Manifest{Module: RuntimeModule, Version: "1", OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}}, Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "typescript", Entrypoints: []string{"src/main.js"}}}}} resolved, err := m.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle, Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } return resolved } func TestABuildsBundlesAreCarriedOnTheResolvedManifest(t *testing.T) { m := aToolsModule(t, "nftables", "tools/index.js") if len(m.Bundles) != 1 { t.Fatalf("the resolved manifest carries %d bundle(s), not the one the build made", len(m.Bundles)) } b := m.Bundles[0] if b.Name != "tools" || b.Digest != bundleDigest || b.Language != "typescript" || b.Source != ArtifactStoreScheme+"nftables/tools/blobs/"+bundleDigest || len(b.Entrypoints) != 1 || b.Entrypoints[0] != "tools/index.js" { t.Errorf("the bundle is carried as %+v", b) } // A repository manifest may not write what the build derives. raw := `{"module":"x","version":"1","build":{"artifacts":[{"name":"t","kind":"bundle","language":"typescript"}]},` + `"bundles":[{"name":"t","source":"s","digest":"` + bundleDigest + `"}]}` if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "bundles") { t.Errorf("a manifest stating its build's output by hand was accepted: %v", err) } } func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) { store := Rendering{ArtifactStore: "anchor.internal:5101", Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}} nftables := aToolsModule(t, "nftables", "tools/index.js") zsh := aToolsModule(t, "zsh", "tools/index.js", "tools/more.js") t.Run("with the runtime, one archive per tools bundle", func(t *testing.T) { r := Resolution{Node: "anchor", Modules: []Manifest{nftables, zsh, theRuntime(t)}} out, err := r.Declaration(store) if err != nil { t.Fatal(err) } archive := fileNamed(out, "nftables."+BundleID("tools")) if archive == nil { t.Fatalf("nftables' tools bundle was not delivered: %v", ids(out)) } if archive["type"] != "archive" || archive["digest"] != bundleDigest || archive["path"] != BundleRoot+"/nftables/tools" { t.Errorf("delivered as %v", archive) } if archive["source"] != "http://anchor.internal:5101/v2/nftables/tools/blobs/"+bundleDigest { t.Errorf("fetched from %v, not through the store as this network reaches it", archive["source"]) } if fileNamed(out, "zsh."+BundleID("tools")) == nil { t.Errorf("zsh's tools bundle was not delivered: %v", ids(out)) } // The runtime's own bundle is run, not loaded: its process delivers it, not an archive. if fileNamed(out, RuntimeModule+"."+BundleID("runtime")) != nil { t.Error("the runtime's own bundle was delivered as an archive beside its process") } }) t.Run("without the runtime, nothing changes", func(t *testing.T) { r := Resolution{Node: "anchor", Modules: []Manifest{nftables, zsh}} out, err := r.Declaration(store) if err != nil { t.Fatal(err) } for _, id := range ids(out) { if strings.Contains(id, BundleID("")) { t.Errorf("%s was delivered to a machine running no runtime to load it", id) } } }) } func ids(out []map[string]any) []string { var names []string for _, r := range out { names = append(names, r["id"].(string)) } return names } // One process per machine runs the runtime from its own bundle, told what it serves and from where, // where its credential is, and who the operator is — restarted when any of that changes. func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { with := Rendering{ArtifactStore: "anchor.internal:5101", Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}} nftables := aToolsModule(t, "nftables", "tools/index.js") // A bundle carrying a daemon beside its tools says which files the runtime loads. showcase := Manifest{Module: "showcase", Version: "1", Tools: []string{"greet"}, Build: &Build{Artifacts: []Artifact{{Name: "code", Kind: ArtifactBundle, Language: "typescript", Entrypoints: []string{"daemon/index.js", "tools/index.js"}, Loads: []string{"tools/index.js"}}}}} showcase, err := showcase.Resolve([]Built{{Name: "code", Kind: ArtifactBundle, Reference: ArtifactStoreScheme + "showcase/code/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } r := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{nftables, showcase, theRuntime(t)}} out, err := r.Declaration(with) if err != nil { t.Fatal(err) } process := fileNamed(out, RuntimeModule+"."+RuntimeProcessID()) if process == nil { t.Fatalf("no runtime process was composed: %v", ids(out)) } if process["type"] != "process" || process["name"] != RuntimeModule || process["digest"] != bundleDigest || process["source"] != "http://anchor.internal:5101/v2/"+RuntimeModule+"/runtime/blobs/"+bundleDigest { t.Errorf("the runtime's process is %v", process) } if fmt.Sprint(process["run"]) != "[node src/main.js]" { t.Errorf("the runtime is run as %v; its bundle's one entrypoint, by its language's interpreter", process["run"]) } env := process["env"].(map[string]string) if env[RuntimeToolModules] != "nftables="+BundleRoot+"/nftables/tools/tools/index.js,"+ "showcase="+BundleRoot+"/showcase/code/tools/index.js" { t.Errorf("the runtime is told to serve %q: every loaded file, by module, and nothing a bundle runs", env[RuntimeToolModules]) } if env[RuntimeBrokerFile] != "/var/lib/mesh/"+RuntimeModule+"/broker" { t.Errorf("the runtime reads its credential at %q, not where the module's own secret is placed", env[RuntimeBrokerFile]) } if env[RuntimeOperatorAccount] != "ops" || env[RuntimeOperatorHome] != "/home/ops" || process["user"] != "ops" { t.Errorf("the operator is not handed to the runtime: %v as %v", env, process["user"]) } // The credential the process reads belongs to the account it runs as, or it could not read it // (to-be 38 WP3); other modules' secrets are left as their manifests say. if credential := fileNamed(out, RuntimeModule+"."+NeedID("broker")); credential == nil || credential["owner"] != "ops" { t.Errorf("the runtime's credential is not the account's to read: %v", credential) } restarts := fmt.Sprint(process["restart-on"]) for _, want := range []string{"nftables." + BundleID("tools"), "showcase." + BundleID("code"), RuntimeModule + "." + NeedID("broker")} { if !strings.Contains(restarts, want) { t.Errorf("the runtime is not restarted when %s changes: %s", want, restarts) } } // After every bundle and the credential, so both exist before it starts. names := ids(out) if names[len(names)-1] != RuntimeModule+"."+RuntimeProcessID() { t.Errorf("the runtime's process is not last: %v", names) } t.Run("a machine with no account runs it as root without the operator words", func(t *testing.T) { out, err := Resolution{Node: "anchor", Modules: []Manifest{nftables, theRuntime(t)}}.Declaration(with) if err != nil { t.Fatal(err) } process := fileNamed(out, RuntimeModule+"."+RuntimeProcessID()) env := process["env"].(map[string]string) if _, set := env[RuntimeOperatorAccount]; set { t.Error("an operator account was named on a machine that has none") } if _, set := process["user"]; set { t.Error("a user was set on a machine with no account") } if credential := fileNamed(out, RuntimeModule+"."+NeedID("broker")); credential == nil || credential["owner"] != nil { t.Errorf("the runtime's credential was given an owner on a machine with no account: %v", credential) } }) t.Run("a runtime module built wrong is refused by name", func(t *testing.T) { two := Manifest{Module: RuntimeModule, Version: "1", OwnSecrets: OwnSecrets{"broker": {Path: "/b"}}, Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "typescript", Entrypoints: []string{"a.js", "b.js"}}}}} resolved, err := two.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle, Reference: ArtifactStoreScheme + "x/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } _, err = Resolution{Node: "anchor", Modules: []Manifest{resolved}}.Declaration(with) if err == nil || !strings.Contains(err.Error(), "entrypoint") { t.Errorf("a runtime bundle with two entrypoints was composed: %v", err) } }) } // 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) } } }