diff --git a/internal/ownmanifest/image_place_test.go b/internal/ownmanifest/image_place_test.go new file mode 100644 index 00000000..155d514e --- /dev/null +++ b/internal/ownmanifest/image_place_test.go @@ -0,0 +1,92 @@ +package ownmanifest + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +// imagesOutOfPlace lists the image artifacts among manifests (module name to its manifest's text) whose Dockerfile +// is not a file inside that module's own directory, root/ (novox/hq ADR 0487 clause 13, which replaced the +// refusal of the image kind of ADR 0480 clause 11). The image builder builds a module from its directory alone. +// It reads the manifest as plain JSON, since the catalogue parser refuses some of these itself. It mirrors mesh-catalog's checks/imageplace: an empty from, an absolute one, one leaving the directory with .., +// or a name that is no file there, is refused. +func imagesOutOfPlace(t *testing.T, root string, manifests map[string][]byte) []string { + t.Helper() + var out []string + for module, raw := range manifests { + var m struct { + Build struct { + Artifacts []struct{ Name, Kind, From string } `json:"artifacts"` + } `json:"build"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatalf("%s does not parse: %v", module, err) + } + for _, a := range m.Build.Artifacts { + if a.Kind != "image" { + continue + } + clean := filepath.Clean(a.From) + bad := a.From == "" || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) + if !bad { + info, err := os.Stat(filepath.Join(root, module, clean)) + bad = err != nil || info.IsDir() + } + if bad { + out = append(out, module+" ("+a.Name+" from "+a.From+")") + } + } + } + return out +} + +// Every image a module of this repository declares is built from a Dockerfile in the module's own directory +// (novox/hq ADR 0487 clause 13). The catalogue's own modules are held to the same by checks/imageplace in mesh-catalog. +func TestEveryImageArtifactBuildsFromADockerfileInItsOwnModuleDirectory(t *testing.T) { + root := filepath.Join("..", "..", "modules") + paths, err := filepath.Glob(filepath.Join(root, "*", "module.json")) + if err != nil || len(paths) == 0 { + t.Fatalf("no module manifests found in this repository's modules: %v", err) + } + manifests := map[string][]byte{} + for _, p := range paths { + raw, err := os.ReadFile(p) + if err != nil { + t.Fatal(err) + } + manifests[filepath.Base(filepath.Dir(p))] = raw + } + if got := imagesOutOfPlace(t, root, manifests); len(got) != 0 { + t.Errorf("these image artifacts do not build from a Dockerfile in their own module's directory "+ + "(ADR 0487 clause 13): %v", got) + } +} + +// The check fails on each bad case and passes on a good one, so it holds something. +func TestAnImageWhoseDockerfileIsOutOfPlaceIsFoundAndOneInPlaceIsNot(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "x", "sub"), 0o755); err != nil { + t.Fatal(err) + } + for _, f := range []string{"x/Dockerfile", "x/sub/Dockerfile", "outside"} { + if err := os.WriteFile(filepath.Join(root, f), nil, 0o644); err != nil { + t.Fatal(err) + } + } + manifest := func(from string) map[string][]byte { + return map[string][]byte{"x": []byte(`{"module":"x","version":"1","build":{"artifacts":[{"name":"server","kind":"image","from":"` + from + `"}]}}`)} + } + for _, from := range []string{"Dockerfile", "sub/Dockerfile", "./Dockerfile"} { + if got := imagesOutOfPlace(t, root, manifest(from)); len(got) != 0 { + t.Errorf("from %q: found %v, want none", from, got) + } + } + for _, from := range []string{"", "/etc/Dockerfile", "../outside", "..", "sub/../../outside", "Missing", "sub"} { + if got := imagesOutOfPlace(t, root, manifest(from)); len(got) != 1 { + t.Errorf("from %q: found %v, want it refused", from, got) + } + } +} diff --git a/internal/ownmanifest/no_image_artifact_test.go b/internal/ownmanifest/no_image_artifact_test.go deleted file mode 100644 index 689a6d68..00000000 --- a/internal/ownmanifest/no_image_artifact_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package ownmanifest - -import ( - "os" - "path/filepath" - "testing" - - "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// builderThatStillBuildsAnImage is the one builder allowed an image built from a Dockerfile until R4g removes it -// (novox/hq ADR 0480 clause 11, ADR 0453 §10, ADR 0452 §1). -const builderThatStillBuildsAnImage = "build-agent" - -// imageArtifactModules lists the modules among manifests (module name to its manifest's text) that declare an -// artifact of kind image, apart from the one builder allowed it. -func imageArtifactModules(t *testing.T, manifests map[string][]byte) []string { - t.Helper() - var out []string - for module, raw := range manifests { - if module == builderThatStillBuildsAnImage { - continue - } - m, err := catalogue.ParseManifest(raw) - if err != nil { - t.Fatalf("%s does not parse: %v", module, err) - } - if m.Build == nil { - continue - } - for _, a := range m.Build.Artifacts { - if a.Kind == "image" { - out = append(out, module+" ("+a.Name+")") - } - } - } - return out -} - -// Every module of this repository but build-agent runs an upstream image with its program mounted in, so none -// declares an image built from a Dockerfile (novox/hq ADR 0480 clause 11). The catalogue's own modules are held to -// the same by a test in mesh-catalog. -func TestNoModuleManifestButBuildAgentsDeclaresAnImage(t *testing.T) { - paths, err := filepath.Glob(filepath.Join("..", "..", "modules", "*", "module.json")) - if err != nil || len(paths) == 0 { - t.Fatalf("no module manifests found in this repository's modules: %v", err) - } - manifests := map[string][]byte{} - for _, p := range paths { - raw, err := os.ReadFile(p) - if err != nil { - t.Fatal(err) - } - manifests[filepath.Base(filepath.Dir(p))] = raw - } - if got := imageArtifactModules(t, manifests); len(got) != 0 { - t.Errorf("these modules declare an artifact of kind image, which only %s may until R4g (ADR 0480 clause 11, "+ - "ADR 0452 §1): %v. Ship the program as a Go bundle or an archive on an upstream image", - builderThatStillBuildsAnImage, got) - } -} - -// The test fails on a manifest with an image artifact, so it holds something. -func TestAnImageArtifactIsFoundInAnyModuleButBuildAgents(t *testing.T) { - image := []byte(`{"module":"x","version":"1","build":{"artifacts":[{"name":"server","kind":"image","from":"Dockerfile"}]}}`) - if got := imageArtifactModules(t, map[string][]byte{"x": image, builderThatStillBuildsAnImage: image}); len(got) != 1 || got[0] != "x (server)" { - t.Fatalf("found %v, want only x (server)", got) - } -}