A bundle compiled to a binary has no entrypoints, and loads had to name one, so a Go bundle could not be served. Its binary is what the runtime starts: loads names the binary, derived when the module lists tools, and the runtime is told the binary's path, delivered like any tools bundle.
435 lines
20 KiB
Go
435 lines
20 KiB
Go
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)
|
|
}
|
|
// A file a word names restarts the runtime when it changes, as it restarted the tool container.
|
|
restarts := fmt.Sprint(process["restart-on"])
|
|
for _, want := range []string{"dash.config", "dash." + NeedID("token")} {
|
|
if !strings.Contains(restarts, want) {
|
|
t.Errorf("the runtime is not restarted when %s changes: %s", want, restarts)
|
|
}
|
|
}
|
|
if strings.Contains(restarts, "dash.unrelated") || strings.Contains(restarts, "dash.mesh-state") {
|
|
t.Errorf("the runtime restarts for something no word names as a file: %s", restarts)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0193: the runtime is told the launcher a build wrote, and the entrypoint itself for a
|
|
// build from before launchers — so the move needs no flag day.
|
|
func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) {
|
|
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
|
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}}
|
|
launched := Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
|
|
Build: &Build{Artifacts: []Artifact{{Name: "tools", Kind: ArtifactBundle, Language: "typescript",
|
|
Entrypoints: []string{"tools/index.js"}}}}}
|
|
launched, err := launched.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle,
|
|
Reference: ArtifactStoreScheme + "dash/tools/blobs/" + bundleDigest, Digest: bundleDigest,
|
|
Launchers: map[string]string{"tools/index.js": "tools/index.serve.mjs"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
older := aToolsModule(t, "nftables", "tools/index.js")
|
|
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{launched, older, theRuntime(t)}}.Declaration(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string)
|
|
want := "dash=" + BundleRoot + "/dash/tools/tools/index.serve.mjs,nftables=" + BundleRoot + "/nftables/tools/tools/index.js"
|
|
if env[RuntimeToolModules] != want {
|
|
t.Errorf("the runtime is told %q, want %q", env[RuntimeToolModules], want)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0193: a runtime compiled to a binary runs itself from its own unpacked bundle.
|
|
func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) {
|
|
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
|
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}}
|
|
goRuntime := Manifest{Module: RuntimeModule, Version: "1",
|
|
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}},
|
|
Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go",
|
|
System: "arch", From: "cmd/node-tools"}}}}
|
|
goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle,
|
|
Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{aToolsModule(t, "nftables", "tools/index.js"), goRuntime}}.Declaration(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())
|
|
if fmt.Sprint(process["run"]) != "[./node-tools]" {
|
|
t.Errorf("a Go runtime is run as %v, want its own binary", process["run"])
|
|
}
|
|
env := process["env"].(map[string]string)
|
|
if env[RuntimeToolModules] == "" || process["user"] != "ops" {
|
|
t.Errorf("the Go runtime is not told what to serve or whose it is: %v %v", env, process["user"])
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0193: a Go tools bundle is served — its binary is what the runtime starts, delivered
|
|
// like any tools bundle, named to the runtime where a TypeScript bundle names its launcher.
|
|
func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) {
|
|
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
|
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}}
|
|
lamp := Manifest{Module: "lamp", Version: "1", Tools: []string{"on"},
|
|
Build: &Build{Artifacts: []Artifact{{Name: "tools", Kind: ArtifactBundle, Language: "go",
|
|
System: "arch", From: "cmd/lamp-tools"}}}}
|
|
if p := lamp.Build.problems("lamp"); len(p) != 0 {
|
|
t.Fatalf("a Go tools bundle was refused: %v", p)
|
|
}
|
|
lamp, err := lamp.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle,
|
|
Reference: ArtifactStoreScheme + "lamp/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fmt.Sprint(lamp.Bundles[0].Loads) != "[lamp-tools]" {
|
|
t.Fatalf("the runtime loads %v from a Go bundle, want its binary", lamp.Bundles[0].Loads)
|
|
}
|
|
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{lamp, theRuntime(t)}}.Declaration(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fileNamed(out, "lamp."+BundleID("tools")) == nil {
|
|
t.Errorf("the Go bundle is not delivered: %v", ids(out))
|
|
}
|
|
env := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string)
|
|
if env[RuntimeToolModules] != "lamp="+BundlePath("lamp", "tools")+"/lamp-tools" {
|
|
t.Errorf("the runtime is told %q, want the binary", env[RuntimeToolModules])
|
|
}
|
|
|
|
// An artifact may say it explicitly; naming anything but the binary is refused.
|
|
said := Manifest{Module: "lamp", Version: "1", Build: &Build{Artifacts: []Artifact{{Name: "tools",
|
|
Kind: ArtifactBundle, Language: "go", System: "arch", Binary: "lamp", Loads: []string{"lamp"}}}}}
|
|
if p := said.Build.problems("lamp"); len(p) != 0 {
|
|
t.Errorf("loads naming the binary was refused: %v", p)
|
|
}
|
|
said.Build.Artifacts[0].Loads = []string{"tools/index.js"}
|
|
if p := said.Build.problems("lamp"); len(p) == 0 {
|
|
t.Error("a Go bundle loading a file it does not contain was admitted")
|
|
}
|
|
}
|