Files
mesh-controller/internal/builder/builder_integration_test.go
T

363 lines
14 KiB
Go

//go:build integration
package builder
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
)
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{}, GitCredential{}, 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{}, GitCredential{}, 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{}, GitCredential{}, 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{}, GitCredential{}, 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{}, GitCredential{}, 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{}, GitCredential{}, 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{}, GitCredential{}, 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{}, GitCredential{}, 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 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{}, GitCredential{}, 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])
}
}
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{}, GitCredential{}, 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)
}
}
}
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{}, GitCredential{}, 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)
}
}
func TestABuildOffersTheForgesCredentialThroughGitsOwnStore(t *testing.T) {
r, workspace := aRepository(t, withBoth, map[string]string{
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
})
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
workspace, nil, Npmrc{},
GitCredential{URL: "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000"}, nil)
if err != nil {
t.Fatal(err)
}
clone := r.ran[0]
stored := storeNamedIn(t, clone)
for _, line := range r.ran {
if strings.Contains(line, "sw0rdfi5h") {
t.Fatalf("the secret is in a command line, readable by anything that can list processes: %s", line)
}
}
// Outside the workspace a check's container mounts, and gone once the build is (novox/hq issue 462); what
// it held while git read it is checked with the check's clones (TestACheckContainerSeesNoForgeCredential).
if withinDir(workspace, stored) {
t.Fatalf("the credential store %s is inside the workspace %s", stored, workspace)
}
if _, err := os.Stat(stored); !os.IsNotExist(err) {
t.Fatalf("the credential store outlives the build: %v", err)
}
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
t.Fatal("a credential file is left in the workspace")
}
}
func TestABuildWithNoCredentialClonesExactlyAsBefore(t *testing.T) {
r, workspace := aRepository(t, withBoth, map[string]string{
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
})
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
workspace, nil, Npmrc{}, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(r.ran[0], "git clone --quiet ") {
t.Fatalf("a credential-less clone grew flags: %s", r.ran[0])
}
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
t.Fatal("a credential file was written with no credential to put in it")
}
}
func TestAContextCloneCarriesTheSameCredentialStore(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,
"Dockerfile": "FROM scratch\nCOPY go.mod ./\n",
},
secondary: map[string]map[string]string{
"https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"},
},
}
workspace := t.TempDir()
_, err := Build(context.Background(), r.run, r,
"https://forge.invalid/catalogue.git", "", "", workspace, nil, Npmrc{},
GitCredential{URL: "https://builder:s3cret@forge.invalid"}, nil)
if err != nil {
t.Fatal(err)
}
stored := storeNamedIn(t, r.ran[0])
var contextClone string
for _, line := range r.ran {
if strings.Contains(line, "clone") && strings.Contains(line, "source.git") {
contextClone = line
}
}
if contextClone == "" {
t.Fatalf("the context was never cloned: %v", r.ran)
}
if !strings.Contains(contextClone, "credential.helper=store --file="+stored) {
t.Fatalf("the context clone does not name the credential store: %s", contextClone)
}
}