201 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|