A bundle that compiled was not yet a bundle that ran. The compiler resolved `import "nats"` from the toolchain image's own node_modules and the pack took only what the compiler wrote, so what a machine unpacked could not find a single dependency — and Node would have read the bare `.js` as CommonJS besides. No TypeScript bundle had run live to show it; the runtime's own is the first that must. A toolchain now names a Dependencies directory in its image, copied whole into the output's root after the compile by a second run in the same image: for TypeScript /app/runtime, which the runtime's image puts a `"type": "module"` package.json and its pruned node_modules at. An older image without it fails the build by name rather than packing a bundle that starts nowhere. The SDK's and the runtime's dependencies, nothing module-specific yet: a skeleton, by ADR 0188 §5.
210 lines
8.7 KiB
Go
210 lines
8.7 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const aBundle = `{"module":"greeter","version":"1",
|
|
"build":{"artifacts":[
|
|
{"name":"code","kind":"bundle","language":"typescript","entrypoints":["index.js"]}]},
|
|
"resources":[
|
|
{"id":"files","type":"archive","path":"/opt/greeter","artifact":"code"}]}`
|
|
|
|
// compiling is a runner that behaves like a toolchain: when asked to compile, it leaves output
|
|
// where the toolchain says output lands. Without this the pack step has nothing to pack, and the
|
|
// test would be asserting on a failure rather than on a build.
|
|
type compiling struct{ *recorded }
|
|
|
|
func (c compiling) run(ctx context.Context, dir, name string, args ...string) (string, error) {
|
|
out, err := c.recorded.run(ctx, dir, name, args...)
|
|
if name != "docker" || len(args) == 0 || args[0] != "run" {
|
|
return out, err
|
|
}
|
|
// **Writes where it was TOLD to**, rather than to a fixed directory. A fake that always wrote
|
|
// to one place would pass whether or not the builder gave each artifact its own — which is the
|
|
// thing being tested.
|
|
where := ""
|
|
for i, a := range args {
|
|
if a == "--outDir" && i+1 < len(args) {
|
|
where = args[i+1]
|
|
}
|
|
}
|
|
if where == "" {
|
|
return out, err
|
|
}
|
|
made := filepath.Join(dir, where)
|
|
if err := os.MkdirAll(made, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(made, "index.js"), []byte("console.log(1)"), 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// **A module says what it is written in, and needs no Dockerfile.** This is the whole point of the
|
|
// bundle recipe: the same module previously needed a hand-written recipe repeating an incantation
|
|
// that is easy to get wrong in ways that fail somewhere else.
|
|
func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) {
|
|
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
|
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
|
|
|
got, err := Build(context.Background(), compiling{r}.run, r,
|
|
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("a module with a language and no Dockerfile did not build: %v", err)
|
|
}
|
|
|
|
// Compiled in the toolchain the mesh chose, not in one the module named.
|
|
var compiled string
|
|
for _, line := range r.ran {
|
|
if strings.HasPrefix(line, "docker run") {
|
|
compiled = line
|
|
}
|
|
}
|
|
if compiled == "" {
|
|
t.Fatalf("nothing was compiled:\n%s", strings.Join(r.ran, "\n"))
|
|
}
|
|
if !strings.Contains(compiled, "mesh-tools/build@sha256:") {
|
|
t.Fatalf("the compile did not run in the mesh's own toolchain: %s", compiled)
|
|
}
|
|
if strings.Contains(strings.Join(r.ran, "\n"), "docker build") {
|
|
t.Fatalf("a bundle invoked a Dockerfile build, which is the thing it exists to avoid:\n%s",
|
|
strings.Join(r.ran, "\n"))
|
|
}
|
|
|
|
// And pinned by a digest of what came out, like any other artifact.
|
|
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
|
if !strings.HasPrefix(digest, "sha256:") {
|
|
t.Fatalf("the bundle was not pinned: %v", got.Manifest.Resources[0])
|
|
}
|
|
|
|
// **And what it runs with, from the image it was compiled in** (novox/hq to-be 38 WP3). A
|
|
// second run in the same toolchain image copies the toolchain's runtime directory — the
|
|
// `"type": "module"` package.json and the pruned node_modules — into the output's root, and
|
|
// refuses by name when the image carries none rather than packing a bundle that starts nowhere.
|
|
var copied string
|
|
for _, line := range r.ran {
|
|
if strings.HasPrefix(line, "docker run") && strings.Contains(line, "/app/runtime") {
|
|
copied = line
|
|
}
|
|
}
|
|
if copied == "" {
|
|
t.Fatalf("the bundle's dependencies were not copied in after the compile:\n%s", strings.Join(r.ran, "\n"))
|
|
}
|
|
if !strings.Contains(copied, "mesh-tools/build@sha256:") || !strings.Contains(copied, "predates") ||
|
|
!strings.Contains(copied, Out("code")) {
|
|
t.Fatalf("the copy does not run in the same toolchain, refuse an older image by name, or land in the artifact's output: %s", copied)
|
|
}
|
|
if strings.Index(strings.Join(r.ran, "\n"), "--outDir") > strings.Index(strings.Join(r.ran, "\n"), "/app/runtime") {
|
|
t.Fatal("the dependencies were copied before the compile wrote its output")
|
|
}
|
|
}
|
|
|
|
// A language whose bundle carries its own dependencies copies nothing in: a Go binary is static.
|
|
func TestOnlyALanguageWithARuntimeDirectoryCopiesDependenciesIn(t *testing.T) {
|
|
ts, _ := ToolchainFor("typescript")
|
|
if ts.Dependencies != "/app/runtime" {
|
|
t.Fatalf("typescript bundles run with %q", ts.Dependencies)
|
|
}
|
|
for _, language := range []string{"go", "python"} {
|
|
chain, _ := ToolchainFor(language)
|
|
if chain.Dependencies != "" {
|
|
t.Fatalf("%s copies %q into every bundle, and its bundles carry their own", language, chain.Dependencies)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Refused before anything is built, naming what to build first.** A base the mesh has not built
|
|
// is not a compile that fails on its first line — it is a question somebody can answer, and saying
|
|
// it early is the difference between a fixable message and one about a missing image.
|
|
func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) {
|
|
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
|
|
|
_, err := Build(context.Background(), compiling{r}.run, r,
|
|
"https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
|
if err == nil {
|
|
t.Fatal("a bundle was built with no toolchain to compile it in")
|
|
}
|
|
if !strings.Contains(err.Error(), "mesh-tools") {
|
|
t.Fatalf("the refusal does not name what has to be built first: %v", err)
|
|
}
|
|
for _, line := range r.ran {
|
|
if strings.HasPrefix(line, "docker run") {
|
|
t.Fatalf("a compile was attempted before the refusal: %s", line)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A language the mesh does not build is refused the same way, and names what it can build.
|
|
func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) {
|
|
manifest := strings.Replace(aBundle, `"language":"typescript"`, `"language":"cobol"`, 1)
|
|
r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "x"})
|
|
|
|
_, err := Build(context.Background(), compiling{r}.run, r,
|
|
"https://forge.invalid/greeter.git", "", "", workspace,
|
|
map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, GitCredential{}, nil)
|
|
if err == nil {
|
|
t.Fatal("a language nothing can compile was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "typescript") {
|
|
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
|
}
|
|
}
|
|
|
|
// **One module, two bundles, and neither packs the other.**
|
|
//
|
|
// The case that matters for real modules: a module is one piece of software and may still carry a
|
|
// daemon in one language and tools in another (ADR 0040). An earlier version of this compiled
|
|
// every bundle into the toolchain's single output directory, so two of them would overwrite each
|
|
// other and then be packed together — one artifact containing both, twice.
|
|
func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) {
|
|
const two = `{"module":"greeter","version":"1",
|
|
"build":{"artifacts":[
|
|
{"name":"daemon","kind":"bundle","language":"typescript","entrypoints":["index.js"]},
|
|
{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"]}]},
|
|
"resources":[
|
|
{"id":"a","type":"archive","path":"/opt/greeter/daemon","artifact":"daemon"},
|
|
{"id":"b","type":"archive","path":"/opt/greeter/tools","artifact":"tools"}]}`
|
|
|
|
r, workspace := aRepository(t, two, map[string]string{"index.ts": "console.log(1)"})
|
|
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
|
|
|
got, err := Build(context.Background(), compiling{r}.run, r,
|
|
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("a module with two bundles did not build: %v", err)
|
|
}
|
|
|
|
// Compiled into two different places. Only the compile lines: the copy of each bundle's
|
|
// dependencies names the same directory again, deliberately.
|
|
var outputs []string
|
|
for _, line := range r.ran {
|
|
if !strings.Contains(line, "--outDir") {
|
|
continue
|
|
}
|
|
for _, part := range strings.Fields(line) {
|
|
if strings.HasPrefix(part, ".mesh-build/") {
|
|
outputs = append(outputs, part)
|
|
}
|
|
}
|
|
}
|
|
if len(outputs) != 2 || outputs[0] == outputs[1] {
|
|
t.Fatalf("two bundles did not get their own output directories: %v", outputs)
|
|
}
|
|
|
|
// And published as two artifacts, each with its own digest.
|
|
if len(r.archives) != 2 {
|
|
t.Fatalf("expected two archives published, got %v", r.archives)
|
|
}
|
|
first, _ := got.Manifest.Resources[0]["digest"].(string)
|
|
second, _ := got.Manifest.Resources[1]["digest"].(string)
|
|
if first == "" || second == "" {
|
|
t.Fatalf("a bundle was not pinned: %v", got.Manifest.Resources)
|
|
}
|
|
}
|