build.on takes {arg, image@sha256:…} beside {arg, module, artifact}: the image is
copied into the mesh's registry before the build (ADR 0096) and the recipe reads the
copy from the argument. A FROM or COPY --from naming a registry image the manifest
did not declare is refused before the build, naming it and the remedy; stages,
declared arguments and scratch are not fetches (novox/hq 04-ISSUES/064, ADR 0097).
132 lines
5.3 KiB
Go
132 lines
5.3 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
|
|
`
|
|
got := undeclaredFetches(recipe, map[string]bool{"RUNTIME_BASE": true})
|
|
want := []string{"${MC_BASE} (a build argument the manifest does not declare)", "vendor/tool:latest", "golang:1.25-alpine"}
|
|
if strings.Join(got, "|") != strings.Join(want, "|") {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
if got := undeclaredFetches(recipe, map[string]bool{"RUNTIME_BASE": true, "MC_BASE": true}); len(got) != 2 {
|
|
t.Fatalf("declared arguments are not fetches: %v", got)
|
|
}
|
|
}
|