package builder import ( "context" "os" "path/filepath" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" ) // A repository becoming artifacts the mesh can pin. // // git and docker are injected rather than run, because what is under test is the ORDER and the // refusals — that nothing is published until everything is built, that a build reads only its own // tree, that two builds of one commit produce one digest. Running docker here would test docker. type recorded struct { ran []string // dirs is the directory each entry in ran was run from, same index — so a test can ask not // only what ran but where. dirs []string images map[string]string archives map[string]string failPush bool // contents is what a clone of this repository lands, so the fake clone can restore the tree // Build deliberately removes first. contents map[string]string // secondary is what a clone of a repository OTHER than the one under test lands, keyed by // that repository's URL — an artifact's own build context, cloned apart from the module. secondary map[string]map[string]string // stamped is the modification time the clone gives every file. Set differently between two // builds of one commit, because otherwise both land in the same second and a packer that // carried timestamps would still produce one digest — which is a test that passes for a // reason that has nothing to do with what it claims. stamped time.Time } func (r *recorded) run(_ context.Context, dir, name string, args ...string) (string, error) { line := name + " " + strings.Join(args, " ") r.ran = append(r.ran, line) r.dirs = append(r.dirs, dir) switch { case name == "git" && len(args) > 0 && args[0] == "clone": repository := args[len(args)-2] tree := args[len(args)-1] if err := os.MkdirAll(tree, 0o755); err != nil { return "", err } lands := r.contents if by, is := r.secondary[repository]; is { lands = by } for path, body := range lands { full := filepath.Join(tree, path) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { return "", err } if err := os.WriteFile(full, []byte(body), 0o644); err != nil { return "", err } if !r.stamped.IsZero() { if err := os.Chtimes(full, r.stamped, r.stamped); err != nil { return "", err } } } return "", nil case name == "git" && len(args) > 0 && args[0] == "rev-parse": return "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff\n", nil } return "", nil } func (r *recorded) PublishImage(_ context.Context, localTag, repository string) (string, error) { if r.failPush { return "", os.ErrPermission } if r.images == nil { r.images = map[string]string{} } r.images[repository] = localTag return "registry.invalid/" + repository + "@sha256:" + strings.Repeat("a", 64), nil } func (r *recorded) PublishArchive(_ context.Context, repository string, body []byte, digest string) (string, error) { if r.failPush { return "", os.ErrPermission } if r.archives == nil { r.archives = map[string]string{} } r.archives[repository] = digest _ = body return "https://store.invalid/" + repository, nil } // aRepository is a workspace whose clone lands a manifest and some files. func aRepository(t *testing.T, manifest string, files map[string]string) (*recorded, string) { t.Helper() contents := map[string]string{ManifestName: manifest} for name, body := range files { contents[name] = body } return &recorded{contents: contents}, t.TempDir() } const withBoth = `{"module":"meshboard","version":"1", "build":{"artifacts":[ {"name":"server","kind":"image","from":"Dockerfile"}, {"name":"look","kind":"archive","from":"files"}]}, "resources":[ {"id":"svc","type":"container","name":"meshboard","artifact":"server"}, {"id":"theme","type":"archive","path":"/opt/meshboard","artifact":"look"}]}` func TestABuildProducesAManifestThePinsAreIn(t *testing.T) { r, workspace := aRepository(t, withBoth, map[string]string{ "Dockerfile": "FROM scratch", "files/theme.conf": "dark", }) got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, nil) if err != nil { t.Fatal(err) } if got.Commit != "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff" { t.Fatalf("the commit was not recorded: %q", got.Commit) } if got.Manifest.Resources[0]["image"] == nil { t.Fatalf("the image was not pinned: %v", got.Manifest.Resources[0]) } digest, _ := got.Manifest.Resources[1]["digest"].(string) if !strings.HasPrefix(digest, "sha256:") { t.Fatalf("the archive was not pinned: %v", got.Manifest.Resources[1]) } } func TestTwoBuildsOfOneCommitProduceOneDigest(t *testing.T) { // Or nothing downstream can tell "this changed" from "this was built again", and every // rebuild looks like a change to every machine holding it. var digests []string for i := 0; i < 2; i++ { r, workspace := aRepository(t, withBoth, map[string]string{ "Dockerfile": "FROM scratch", "files/a.conf": "one", "files/b.conf": "two", }) // A year apart, so a packer carrying timestamps cannot accidentally agree. r.stamped = time.Date(2020+i, time.March, 3, 4, 5, 6, 0, time.UTC) got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil) if err != nil { t.Fatal(err) } for _, b := range got.Built { if b.Kind == catalogue.ArtifactArchive { digests = append(digests, b.Digest) } } } if digests[0] != digests[1] { t.Fatalf("two builds of one commit produced %s and %s", digests[0], digests[1]) } } func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) { // Half a module in the store under a digest the mesh never records is reachable, // unreferenced, and indistinguishable from something in use. r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch"}) // `files` is missing, so packing the archive fails — after the image would have been pushed. _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil) if err == nil { t.Fatal("a build with a missing input succeeded") } if len(r.archives) != 0 { t.Fatalf("an archive was published by a failed build: %v", r.archives) } } func TestARepositoryWithNoManifestSaysSo(t *testing.T) { workspace := t.TempDir() r := &recorded{contents: map[string]string{"README.md": "nothing to see"}} _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil) if err == nil { t.Fatal("a repository with nothing saying what it is was built") } if !strings.Contains(err.Error(), ManifestName) { t.Fatalf("the failure does not name what is missing: %v", err) } } func TestAModuleThatBuildsNothingStillProducesAManifest(t *testing.T) { // Most of what a person installs is configuration. r, workspace := aRepository(t, `{"module":"shell","version":"1","resources":[ {"id":"rc","type":"file","path":"/etc/zsh/zshrc","content":"setopt"}]}`, nil) got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, nil) if err != nil { t.Fatal(err) } if len(got.Built) != 0 { t.Fatalf("something was built: %v", got.Built) } if got.Manifest.Module != "shell" || len(got.Manifest.Resources) != 1 { t.Fatalf("got %+v", got.Manifest) } for _, line := range r.ran { if strings.HasPrefix(line, "docker") { t.Fatalf("docker was run for a module that builds nothing: %q", line) } } } func TestTheTreeIsFreshEveryTime(t *testing.T) { // A build that reuses a working tree can succeed because of something a previous build left // behind, and that is a build nobody can reproduce. r, workspace := aRepository(t, withBoth, map[string]string{ "Dockerfile": "FROM scratch", "files/a": "b", }) leftover := filepath.Join(workspace, "source", "files", "from-last-time") if err := os.MkdirAll(filepath.Dir(leftover), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(leftover, []byte("stale"), 0o644); err != nil { t.Fatal(err) } if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil); err != nil { t.Fatal(err) } if _, err := os.Stat(leftover); err == nil { t.Fatal("a previous build's file survived into this one") } } func TestABuildThatCannotPushFails(t *testing.T) { r, workspace := aRepository(t, withBoth, map[string]string{ "Dockerfile": "FROM scratch", "files/a": "b", }) r.failPush = true if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil); err == nil { t.Fatal("a build that could publish nothing reported success") } } func TestAnUpstreamImageIsMirroredRatherThanBuilt(t *testing.T) { // A module usually runs software it did not write. Naming the upstream reference directly // would need every machine to reach a public registry, and would pin to a tag somebody else // can move. const mirrors = `{"module":"postgres","version":"1", "build":{"artifacts":[{"name":"store","kind":"upstream","from":"postgres:17-alpine"}]}, "resources":[{"id":"db","type":"container","name":"mesh-postgres","artifact":"store"}]}` r, workspace := aRepository(t, mirrors, nil) got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, nil) if err != nil { t.Fatal(err) } // Pulled, not built. var pulled, built bool for _, line := range r.ran { if strings.HasPrefix(line, "docker pull postgres:17-alpine") { pulled = true } if strings.HasPrefix(line, "docker build") { built = true } } if !pulled { t.Fatalf("the upstream image was not fetched: %v", r.ran) } if built { t.Fatalf("something was built for an image that is mirrored: %v", r.ran) } // And the resource names what this registry serves, pinned by the digest it assigned. image, _ := got.Manifest.Resources[0]["image"].(string) if !strings.Contains(image, "@sha256:") { t.Fatalf("the mirrored image is not pinned by digest: %q", image) } if strings.Contains(image, "17-alpine") { t.Fatalf("the resource still names the upstream tag: %q", image) } } func TestAnUpstreamImageWithNoTagIsRefused(t *testing.T) { // What gets mirrored would be whatever `latest` means today, and a module pinned to that is // not pinned. _, err := catalogue.ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ {"name":"x","kind":"upstream","from":"postgres"}]}}`)) if err == nil { t.Fatal("an untagged upstream reference was accepted") } if !strings.Contains(err.Error(), "no tag or digest") { t.Fatalf("unhelpful refusal: %v", err) } } func TestAnUpstreamReferenceIsNotAPathInTheRepository(t *testing.T) { // The rule that a build reads only its own repository must not refuse every reference with a // registry host in it. if _, err := catalogue.ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ {"name":"x","kind":"upstream","from":"registry.example/library/postgres:17"}]}}`)); err != nil { t.Fatalf("a perfectly ordinary upstream reference was refused: %v", err) } } // **A module is a repository and a path within it** (novox/hq ADR 0069). The catalogue holds its // modules one to a directory and the system this replaces has always built one that way, so a // builder that could only read a repository's root could build none of what exists. func TestAModuleIsBuiltFromItsPathWithinTheRepository(t *testing.T) { r := &recorded{contents: map[string]string{ "README.md": "this repository holds several modules", "modules/shell/" + ManifestName: withBoth, "modules/shell/Dockerfile": "FROM scratch", "modules/shell/files/theme.conf": "dark", // A second module beside it, so what is built is chosen by the path rather than by // happening to be the only manifest in the clone. "modules/other/" + ManifestName: `{"module":"other","version":"1"}`, }} got, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, nil) if err != nil { t.Fatal(err) } if got.Manifest.Module != "meshboard" { t.Fatalf("built %q, which is not the module at the path asked for", got.Manifest.Module) } if got.Manifest.Resources[0]["image"] == nil { t.Fatalf("the image was not pinned: %v", got.Manifest.Resources[0]) } } // A build reads only its own tree. A path climbing out of the clone would otherwise let a build // read — and an archive artifact publish — whatever the build machine happens to hold, which is // the one thing a machine that builds other people's repositories must not do. func TestAPathThatLeavesTheRepositoryIsRefused(t *testing.T) { for _, escaping := range []string{"../../etc", "/etc"} { r := &recorded{contents: map[string]string{ManifestName: withBoth}} _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, nil) if err == nil { t.Fatalf("%q was accepted as a module's path", escaping) } if !strings.Contains(err.Error(), "leaves the repository") && !strings.Contains(err.Error(), "absolute path") { t.Fatalf("the refusal of %q does not say why: %v", escaping, err) } } } // **Packaging and source are allowed to live apart** — a module that ships only the recipe for // source that lives in a second repository (the reference route-proxy, packaged in the catalogue // but built from mesh-controller's own repository) names where that source actually is, rather // than vendoring a second copy the two could drift from. func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) { const withContext = `{"module":"route-proxy","version":"1", "build":{"artifacts":[ {"name":"server","kind":"image","from":"Dockerfile", "context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}` r := &recorded{ contents: map[string]string{ ManifestName: withContext, // The recipe lives with the packaging, not the source — read from here regardless of // where the build context comes from. FROM scratch declares no base, so what is under // test — where the context comes from — is not entangled with ADR 0097's own checks. "Dockerfile": "FROM scratch\nCOPY go.mod ./\n", }, secondary: map[string]map[string]string{ // go.mod exists only in the second repository. A build context taken from the wrong // place would never find it, which a real docker build would refuse on — the fake // does not read files, so what is checked below is that the build was even pointed // at the right place, not that COPY would have succeeded. "https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"}, }, } _, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, nil) if err != nil { t.Fatal(err) } var clonedSource bool for _, line := range r.ran { if strings.HasPrefix(line, "git clone") && strings.Contains(line, "https://forge.invalid/source.git") { clonedSource = true } } if !clonedSource { t.Fatalf("the artifact's own context was never cloned: %v", r.ran) } buildIndex := -1 for i, line := range r.ran { if strings.HasPrefix(line, "docker build ") { buildIndex = i } } if buildIndex == -1 { t.Fatal("no docker build was run") } build := r.ran[buildIndex] buildDir := r.dirs[buildIndex] if !strings.Contains(buildDir, "context-server") { t.Errorf("docker build ran from %q, not the artifact's own cloned context", buildDir) } recipe := strings.SplitN(strings.SplitN(build, "-f ", 2)[1], " ", 2)[0] if !filepath.IsAbs(recipe) { t.Errorf("the recipe %q is not an absolute path, so it is read relative to whatever "+ "directory the build context moved to rather than where it actually is", recipe) } if !strings.HasSuffix(recipe, string(filepath.Separator)+"Dockerfile") { t.Errorf("the recipe is not the module's own Dockerfile: %q", recipe) } if !strings.HasSuffix(build, " .") { t.Errorf("the build was not given a context: %s", build) } }