Every served bundle is its own process now, so each carries its own copy of what it imports: after the compile and the launchers, the toolchain image's esbuild bundles every entrypoint in place and every launcher under its own name into one ES module file, the SDK inlined, require provided to inlined CommonJS, the launcher's shebang kept and its mode 0755. The toolchain's node_modules is copied only for packages an artifact names external. An image without the bundler is refused by name. Issue 212: build.on already passes a published package by its exact version and plans the toolchain after it; tests say so.
223 lines
9.4 KiB
Go
223 lines
9.4 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// A module naming a base the mesh has not built is refused, and the refusal names what is missing.
|
|
//
|
|
// **This is the whole point of naming a base rather than pinning one** (novox/hq issue 044). A
|
|
// recipe with a fingerprint typed into it fails inside a container runtime, on its first line, with
|
|
// a message about an image nobody can look up. This fails before anything is built, saying which
|
|
// module has to exist first.
|
|
func TestABaseTheMeshHasNotBuiltIsRefused(t *testing.T) {
|
|
manifest := catalogue.Manifest{
|
|
Module: "postgres",
|
|
Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}},
|
|
},
|
|
}
|
|
_, _, err := standingOn(context.Background(), manifest, map[string]string{}, noMirror)
|
|
if err == nil {
|
|
t.Fatal("a base nothing has built was accepted; the build would have failed on its first line")
|
|
}
|
|
for _, want := range []string{"mesh-tools", "postgres"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not name %s: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And one the mesh holds becomes the argument the recipe reads it from.
|
|
func TestABaseTheMeshHoldsBecomesABuildArgument(t *testing.T) {
|
|
manifest := catalogue.Manifest{
|
|
Module: "postgres",
|
|
Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}},
|
|
},
|
|
}
|
|
held := map[string]string{"mesh-tools/runtime": "127.0.0.1:5000/mesh-tools/runtime@sha256:" + strings.Repeat("a", 64)}
|
|
args, _, err := standingOn(context.Background(), manifest, held, noMirror)
|
|
if err != nil {
|
|
t.Fatalf("a base this mesh holds was refused: %v", err)
|
|
}
|
|
want := []string{"--build-arg", "RUNTIME_BASE=" + held["mesh-tools/runtime"]}
|
|
if len(args) != len(want) || args[0] != want[0] || args[1] != want[1] {
|
|
t.Fatalf("the build was invoked with %v, not %v", args, want)
|
|
}
|
|
}
|
|
|
|
// A module naming no base asks for nothing, which is most modules.
|
|
func TestAModuleNamingNoBaseAddsNoArguments(t *testing.T) {
|
|
args, _, err := standingOn(context.Background(), catalogue.Manifest{Module: "hello-web", Build: &catalogue.Build{}}, nil, noMirror)
|
|
if err != nil || args != nil {
|
|
t.Fatalf("a module naming no base produced %v, %v", args, err)
|
|
}
|
|
}
|
|
|
|
// Half a base is refused rather than half-applied.
|
|
func TestAnIncompleteBaseIsRefused(t *testing.T) {
|
|
manifest := catalogue.Manifest{
|
|
Module: "postgres",
|
|
Build: &catalogue.Build{On: []catalogue.BuildsOn{{Module: "mesh-tools", Artifact: "runtime"}}},
|
|
}
|
|
if _, _, err := standingOn(context.Background(), manifest, map[string]string{"mesh-tools/runtime": "x"}, noMirror); err == nil {
|
|
t.Fatal("a base with no build argument was accepted; nothing would have read it")
|
|
}
|
|
}
|
|
|
|
// noMirror is a mirror for tests whose bases are all the mesh's own.
|
|
func noMirror(context.Context, string, string) (string, error) {
|
|
return "", fmt.Errorf("nothing to copy in this test")
|
|
}
|
|
|
|
// A build may stand on an image published elsewhere, declared and pinned (novox/hq 04-ISSUES/064,
|
|
// ADR 0097): it is copied into the mesh's registry first and the recipe is handed the copy.
|
|
func TestADeclaredVendorImageIsCopiedInAndHandedToTheRecipe(t *testing.T) {
|
|
manifest := catalogue.Manifest{
|
|
Module: "minio",
|
|
Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{{Arg: "MC_BASE", Image: "quay.io/minio/mc@sha256:" + strings.Repeat("c", 64)}},
|
|
},
|
|
}
|
|
var asked []string
|
|
args, _, err := standingOn(context.Background(), manifest, nil, func(_ context.Context, from, repository string) (string, error) {
|
|
asked = append(asked, from+" -> "+repository)
|
|
return "127.0.0.1:5000/" + repository + "@sha256:" + strings.Repeat("d", 64), nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked) != 1 || asked[0] != "quay.io/minio/mc@sha256:"+strings.Repeat("c", 64)+" -> minio/on-mc_base" {
|
|
t.Fatalf("the image was not copied under the module's repository: %v", asked)
|
|
}
|
|
if strings.Join(args, " ") != "--build-arg MC_BASE=127.0.0.1:5000/minio/on-mc_base@sha256:"+strings.Repeat("d", 64) {
|
|
t.Fatalf("the recipe was not handed the copy: %v", args)
|
|
}
|
|
// Unpinned, it is refused: a tag is what somebody else can move.
|
|
manifest.Build.On[0].Image = "quay.io/minio/mc:latest"
|
|
if _, _, err := standingOn(context.Background(), manifest, nil, noMirror); err == nil || !strings.Contains(err.Error(), "not pinned") {
|
|
t.Fatalf("an unpinned vendor image was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
// A recipe reaching for an image the manifest did not declare is named, and its own stages,
|
|
// declared arguments and scratch are not.
|
|
func TestARecipeFetchingWhatTheManifestDidNotDeclareIsNamed(t *testing.T) {
|
|
recipe := `
|
|
ARG RUNTIME_BASE
|
|
ARG MC_BASE
|
|
FROM ${RUNTIME_BASE} AS build
|
|
COPY --from=${MC_BASE} /usr/bin/mc /usr/local/bin/mc
|
|
COPY --from=build /out /out
|
|
COPY --from=0 /x /x
|
|
FROM scratch
|
|
COPY --from=vendor/tool:latest /tool /tool
|
|
FROM golang:1.25-alpine AS go
|
|
`
|
|
bases, copies := undeclaredFetches(recipe, map[string]bool{"RUNTIME_BASE": true})
|
|
if strings.Join(copies, "|") != "${MC_BASE} (a build argument the manifest does not declare)|vendor/tool:latest" {
|
|
t.Fatalf("copies out of undeclared images: %v", copies)
|
|
}
|
|
// A base fetched on its own is named apart, and refused like a copy (ADR 0097).
|
|
if strings.Join(bases, "|") != "golang:1.25-alpine" {
|
|
t.Fatalf("undeclared bases: %v", bases)
|
|
}
|
|
if _, copies := undeclaredFetches(recipe, map[string]bool{"RUNTIME_BASE": true, "MC_BASE": true}); len(copies) != 1 {
|
|
t.Fatalf("declared arguments are not fetches: %v", copies)
|
|
}
|
|
}
|
|
|
|
// Continued lines are one instruction, heredoc bodies are not instructions, and a RUN --mount reaches
|
|
// for an image as a COPY --from does (review C4, C5).
|
|
func TestARecipeIsReadAsInstructions(t *testing.T) {
|
|
recipe := "ARG RUNTIME_BASE\n" +
|
|
"FROM ${RUNTIME_BASE} \\\n AS build\n" +
|
|
"COPY \\\n --from=docker.io/vendor/one:latest /a /a\n" +
|
|
"COPY --from=build /out /out\n" +
|
|
"COPY <<EOF /app/x.py\nfrom os import path\nEOF\n" +
|
|
"RUN --mount=type=bind,from=docker.io/vendor/two:1,target=/t cp /t/x /x\n" +
|
|
"FROM scratch\n"
|
|
bases, copies := undeclaredFetches(recipe, map[string]bool{"RUNTIME_BASE": true})
|
|
if strings.Join(copies, "|") != "docker.io/vendor/one:latest|docker.io/vendor/two:1" {
|
|
t.Fatalf("copies: %v", copies)
|
|
}
|
|
if len(bases) != 0 {
|
|
t.Fatalf("a heredoc line or a continued stage was read as a base: %v", bases)
|
|
}
|
|
}
|
|
|
|
// What a build was handed as its bases is what it stood on — recorded, so a changed base knows what
|
|
// to rebuild (novox/hq 04-ISSUES/131). A recipe reads the base from an argument, so nothing else
|
|
// could know.
|
|
func TestTheBasesABuildWasHandedAreWhatItStoodOn(t *testing.T) {
|
|
manifest := catalogue.Manifest{
|
|
Module: "gitea",
|
|
Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{
|
|
{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"},
|
|
{Arg: "BUILD_BASE", Module: "mesh-tools", Artifact: "build"},
|
|
},
|
|
Artifacts: []catalogue.Artifact{{Name: "runtime", Kind: catalogue.ArtifactImage, From: "Dockerfile"}},
|
|
},
|
|
}
|
|
held := map[string]string{
|
|
"mesh-tools/runtime": "127.0.0.1:5000/mesh-tools/runtime@sha256:" + strings.Repeat("a", 64),
|
|
"mesh-tools/build": "127.0.0.1:5000/mesh-tools/build@sha256:" + strings.Repeat("b", 64),
|
|
}
|
|
_, resolved, err := standingOn(context.Background(), manifest, held, noMirror)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := against(t.TempDir(), manifest, resolved)
|
|
if len(got) != 2 || got[0] != held["mesh-tools/build"] || got[1] != held["mesh-tools/runtime"] {
|
|
t.Fatalf("the bases the build was handed were not what it stood on: %v", got)
|
|
}
|
|
}
|
|
|
|
// What a build read besides its module's own repository is the second repository its recipes name,
|
|
// each once: a module that packages source living elsewhere is affected when that source moves.
|
|
func TestWhatABuildReadIsTheRepositoriesItsRecipesName(t *testing.T) {
|
|
elsewhere := catalogue.ArtifactContext{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "main"}
|
|
manifest := catalogue.Manifest{
|
|
Module: "builder",
|
|
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
|
|
{Name: "server", Kind: catalogue.ArtifactImage, From: "Dockerfile", Context: &elsewhere},
|
|
{Name: "tools", Kind: catalogue.ArtifactImage, From: "Dockerfile", Context: &elsewhere},
|
|
{Name: "config", Kind: catalogue.ArtifactArchive, From: "etc"},
|
|
}},
|
|
}
|
|
read := readBy(manifest)
|
|
if len(read) != 1 || read[0] != elsewhere {
|
|
t.Fatalf("the repositories this build read are %+v", read)
|
|
}
|
|
if readBy(catalogue.Manifest{Module: "gitea", Build: &catalogue.Build{}}) != nil {
|
|
t.Fatal("a module whose recipes name no other repository read one")
|
|
}
|
|
}
|
|
|
|
// novox/hq 04-ISSUES/212: a toolchain stands on the SDK's published package, and is built with the
|
|
// exact version the mesh published — an argument that changes when the SDK does, so a rebuild after
|
|
// a release never reuses an install of the version before it.
|
|
func TestAPackageTheMeshPublishedIsPassedByItsExactVersion(t *testing.T) {
|
|
manifest := catalogue.Manifest{
|
|
Module: "mesh-tools",
|
|
Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{{Arg: "MESH_SDK", Module: "mesh-sdk", Artifact: "lib"}},
|
|
},
|
|
}
|
|
held := map[string]string{"mesh-sdk/lib": "@novox/mesh-sdk@0.1.6"}
|
|
args, resolved, err := standingOn(context.Background(), manifest, held, noMirror)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fmt.Sprint(args) != "[--build-arg MESH_SDK=@novox/mesh-sdk@0.1.6]" || fmt.Sprint(resolved) != "[@novox/mesh-sdk@0.1.6]" {
|
|
t.Errorf("the package was passed as %v, recorded as %v", args, resolved)
|
|
}
|
|
}
|