Files
mesh-catalog/modules/mesh-build-image/cmd/mesh-build-image/build_test.go
T

201 lines
7.3 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The fixture module (testdata/repos/image): a Dockerfile that adds a file to an upstream base the manifest pins in
// build.on, as a workspace's tree holds it.
func place(t *testing.T, m *Module, edit func(string) string) {
t.Helper()
src := filepath.Join("testdata", "repos", "image", "modules", "demo")
dst := filepath.Join(m.Work, "ws1", "tree", "modules", "demo")
if err := os.MkdirAll(dst, 0o755); err != nil {
t.Fatal(err)
}
for _, n := range []string{"Dockerfile", "hello.txt", "module.json.fixture"} {
raw, err := os.ReadFile(filepath.Join(src, n))
if err != nil {
t.Fatal(err)
}
if n == "module.json.fixture" {
n = "module.json"
raw = []byte(edit(string(raw)))
}
if err := os.WriteFile(filepath.Join(dst, n), raw, 0o644); err != nil {
t.Fatal(err)
}
}
}
func same(s string) string { return s }
func runBuild(t *testing.T, m *Module, states *keptStates, extra map[string]any) (BuildResult, delivery.Step) {
t.Helper()
args := map[string]any{"workspace": "ws1", "module": "modules/demo", "artifact": "server"}
for k, v := range extra {
args[k] = v
}
if _, err := tool(t, m, BuildTool)(args); err != nil {
t.Fatal(err)
}
st := states.waitEnd(t)
var res BuildResult
if st.State == delivery.StepPassed {
if err := json.Unmarshal(st.Result, &res); err != nil {
t.Fatal(err)
}
}
return res, st
}
func setup(t *testing.T, edit func(string) string) (*Module, *fake, *keptStates) {
t.Helper()
f := newFake(t)
states := newStates()
m := moduleWith(t, f, states)
place(t, m, edit)
return m, f, states
}
// 1703c/AC: the build runs buildah bud rootless: the store and the runtime state in the store directory, the
// Dockerfile of the module's own directory and that directory as the context, the target, the pinned base as a build
// argument; no mount, no credential file, a runtime directory of its own, and it answers the image's digest.
func TestBuildRunsBuildahRootlessAndAnswersTheDigest(t *testing.T) {
m, f, states := setup(t, same)
res, st := runBuild(t, m, states, nil)
if st.State != delivery.StepPassed {
t.Fatalf("step = %+v", st)
}
if res.Digest != imageDigest || res.Artifact != "server" || res.Pushed || res.Reference != "" {
t.Errorf("result = %+v, want the local digest and no push", res)
}
bud := f.called("bud")
if len(bud) != 1 {
t.Fatalf("buildah bud ran %d times", len(bud))
}
dir := filepath.Join(m.Work, "ws1", "tree", "modules", "demo")
for _, want := range []string{
"--root " + m.Store + "/root", "--runroot " + m.Store + "/run", "--isolation oci",
"--file " + dir + "/Dockerfile", "--target final",
"--build-arg DEMO_BASE=docker.io/library/busybox@sha256:0000000000000000000000000000000000000000000000000000000000000001",
} {
if !strings.Contains(bud[0].line, want) {
t.Errorf("bud line lacks %q:\n%s", want, bud[0].line)
}
}
if !strings.HasSuffix(bud[0].line, " "+dir) {
t.Errorf("the context is not the Dockerfile's directory:\n%s", bud[0].line)
}
for _, c := range f.calls {
if c.name != "buildah" {
continue
}
for _, bad := range []string{" -v ", "--volume", "--mount", "--authfile", "--creds", "--cap-add", "--privileged", "--device"} {
if strings.Contains(" "+c.line+" ", bad) {
t.Errorf("%q carries %q", c.line, bad)
}
}
env := strings.Join(c.env, " ")
if strings.Contains(env, "REGISTRY_AUTH") || strings.Contains(env, "DOCKER_CONFIG") || strings.Contains(env, "MESH_") {
t.Errorf("the environment of %q carries a credential or the module's own: %s", c.line, env)
}
if !strings.Contains(env, "XDG_RUNTIME_DIR="+m.Store+"/xdg") {
t.Errorf("the environment of %q lacks the module's own runtime directory: %s", c.line, env)
}
}
// The image is removed from the store once the digest is read, so the store does not grow with every build.
if len(f.called("rmi")) != 1 {
t.Errorf("rmi ran %d times, want the image removed once", len(f.called("rmi")))
}
}
// 1703c: a failing Dockerfile step fails the step, saying buildah's last line.
func TestBuildFailsWithBuildahsLastLine(t *testing.T) {
m, f, states := setup(t, same)
f.failBud = "Error: building at STEP \"RUN false\": exit status 1"
_, st := runBuild(t, m, states, nil)
if st.State != delivery.StepFailed || !strings.Contains(st.Reason, `STEP "RUN false"`) {
t.Errorf("step = %+v", st)
}
if len(f.called("push")) != 0 {
t.Error("a failed build pushed")
}
}
// 1703c: an image whose context is a repository, or whose build.on names another module's artifact, is refused
// (not failed), the reason saying why; nothing is built.
func TestBuildRefusesAnImageWithARepositoryContextOrAModuleBase(t *testing.T) {
cases := map[string]struct {
edit func(string) string
want []string
}{
"context": {func(s string) string {
return strings.Replace(s, `"target": "final"`, `"target": "final", "context": {"seat": "git", "repository": "novox/mesh-controller"}`, 1)
}, []string{"novox/mesh-controller", "Dockerfile's directory", "ADR 0487"}},
"module base": {func(s string) string {
return strings.Replace(s, `"image": "docker.io/library/busybox@sha256:0000000000000000000000000000000000000000000000000000000000000001"`, `"module": "mesh-sdk", "artifact": "lib"`, 1)
}, []string{"artifact lib of the module mesh-sdk", "DEMO_BASE"}},
}
for name, c := range cases {
m, f, states := setup(t, c.edit)
_, st := runBuild(t, m, states, nil)
if st.State != delivery.StepRefused {
t.Errorf("%s: step = %+v, want refused", name, st)
continue
}
for _, w := range c.want {
if !strings.Contains(st.Reason, w) {
t.Errorf("%s: the reason %q lacks %q", name, st.Reason, w)
}
}
if len(f.called("bud")) != 0 {
t.Errorf("%s: a refused image was built", name)
}
}
}
// The refusal is one small function: it knows the two forms and passes the rest.
func TestUnsupportedImageNamesTheTwoFormsAndPassesTheRest(t *testing.T) {
plain := catalogue.Manifest{Module: "x", Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "A", Image: "i@sha256:aa"}}}}
if why := unsupportedImage(plain, catalogue.Artifact{Name: "a", Kind: "image"}); why != "" {
t.Errorf("a plain image was refused: %s", why)
}
if why := unsupportedImage(plain, catalogue.Artifact{Name: "a", Kind: "image", Context: &catalogue.ArtifactContext{Repository: "o/r"}}); why == "" {
t.Error("a repository context passed")
}
}
func TestBuildFailsForWhatIsNotAnImageOrNotThere(t *testing.T) {
for name, tc := range map[string]struct {
edit func(string) string
args map[string]any
}{
"not an image": {func(s string) string { return strings.Replace(s, `"kind": "image"`, `"kind": "archive"`, 1) }, nil},
"no artifact": {same, map[string]any{"artifact": "other"}},
"no Dockerfile": {func(s string) string {
return strings.Replace(s, `"from": "Dockerfile"`, `"from": "Missing"`, 1)
}, nil},
"leaves the module": {func(s string) string {
return strings.Replace(s, `"from": "Dockerfile"`, `"from": "../x/Dockerfile"`, 1)
}, nil},
"outside the workspace": {same, map[string]any{"module": "../.."}},
} {
m, f, states := setup(t, tc.edit)
_, st := runBuild(t, m, states, tc.args)
if st.State != delivery.StepFailed {
t.Errorf("%s: step = %+v, want failed", name, st)
}
if len(f.called("bud")) != 0 {
t.Errorf("%s: buildah bud ran", name)
}
}
}