package build import ( "archive/tar" "bytes" "io" "os" "path/filepath" "runtime" "strings" "testing" "github.com/docker/docker/pkg/archive" "github.com/moby/patternmatcher" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) const dockerfileContents = "FROM busybox" func prepareEmpty(_ *testing.T) string { return "" } func prepareNoFiles(t *testing.T) string { return createTestTempDir(t) } func prepareOneFile(t *testing.T) string { contextDir := createTestTempDir(t) createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) return contextDir } func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) string, excludes []string) { contextDir := prepare(t) err := ValidateContextDirectory(contextDir, excludes) assert.NilError(t, err) } func TestGetContextFromLocalDirNoDockerfile(t *testing.T) { contextDir := createTestTempDir(t) _, _, err := GetContextFromLocalDir(contextDir, "") assert.ErrorContains(t, err, "Dockerfile") } func TestGetContextFromLocalDirNotExistingDir(t *testing.T) { contextDir := createTestTempDir(t) fakePath := filepath.Join(contextDir, "fake") _, _, err := GetContextFromLocalDir(fakePath, "") assert.ErrorContains(t, err, "fake") } func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) { contextDir := createTestTempDir(t) fakePath := filepath.Join(contextDir, "fake") _, _, err := GetContextFromLocalDir(contextDir, fakePath) assert.ErrorContains(t, err, "fake") } func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { contextDir := createTestTempDir(t) createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) chdir(t, contextDir) absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) } func TestGetContextFromLocalDirWithDockerfile(t *testing.T) { contextDir := createTestTempDir(t) createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) } func TestGetContextFromLocalDirLocalFile(t *testing.T) { contextDir := createTestTempDir(t) createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) testFilename := createTestTempFile(t, contextDir, "tmpTest", "test") absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "") if err == nil { t.Fatalf("Error should not be nil") } if absContextDir != "" { t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) } if relDockerfile != "" { t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) } } func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) { contextDir := createTestTempDir(t) chdir(t, contextDir) createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName) assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) } func TestGetContextFromReaderString(t *testing.T) { tarArchive, relDockerfile, err := GetContextFromReader(io.NopCloser(strings.NewReader(dockerfileContents)), "") if err != nil { t.Fatalf("Error when executing GetContextFromReader: %s", err) } tarReader := tar.NewReader(tarArchive) _, err = tarReader.Next() if err != nil { t.Fatalf("Error when reading tar archive: %s", err) } buff := new(bytes.Buffer) buff.ReadFrom(tarReader) contents := buff.String() _, err = tarReader.Next() if err != io.EOF { t.Fatalf("Tar stream too long: %s", err) } assert.NilError(t, tarArchive.Close()) if dockerfileContents != contents { t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) } if relDockerfile != DefaultDockerfileName { t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) } } func TestGetContextFromReaderTar(t *testing.T) { contextDir := createTestTempDir(t) createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) tarStream, err := archive.Tar(contextDir, archive.Uncompressed) assert.NilError(t, err) tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName) assert.NilError(t, err) tarReader := tar.NewReader(tarArchive) header, err := tarReader.Next() assert.NilError(t, err) if header.Name != DefaultDockerfileName { t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name) } buff := new(bytes.Buffer) buff.ReadFrom(tarReader) contents := buff.String() _, err = tarReader.Next() if err != io.EOF { t.Fatalf("Tar stream too long: %s", err) } assert.NilError(t, tarArchive.Close()) if dockerfileContents != contents { t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) } if relDockerfile != DefaultDockerfileName { t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) } } func TestValidateContextDirectoryEmptyContext(t *testing.T) { // This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81. // The test will ultimately end up calling filepath.Abs(""). On Windows, // golang will error. On Linux, golang will return /. Due to there being // drive letters on Windows, this is probably the correct behaviour for // Windows. if runtime.GOOS == "windows" { t.Skip("Invalid test on Windows") } testValidateContextDirectory(t, prepareEmpty, []string{}) } func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) { testValidateContextDirectory(t, prepareNoFiles, []string{}) } func TestValidateContextDirectoryWithOneFile(t *testing.T) { testValidateContextDirectory(t, prepareOneFile, []string{}) } func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) { testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName}) } // createTestTempDir creates a temporary directory for testing. It returns the // created path. When an error occurs, it terminates the test. func createTestTempDir(t *testing.T) string { t.Helper() path := t.TempDir() // Eval Symlinks is needed to account for macOS TMP using symlinks path, err := filepath.EvalSymlinks(path) assert.NilError(t, err) return path } // createTestTempFile creates a temporary file within dir with specific contents and permissions. // When an error occurs, it terminates the test func createTestTempFile(t *testing.T, dir, filename, contents string) string { t.Helper() filePath := filepath.Join(dir, filename) err := os.WriteFile(filePath, []byte(contents), 0o777) assert.NilError(t, err) return filePath } // chdir changes current working directory to dir. // It returns a function which changes working directory back to the previous one. // This function is meant to be executed as a deferred call. // When an error occurs, it terminates the test. func chdir(t *testing.T, dir string) { workingDirectory, err := os.Getwd() assert.NilError(t, err) assert.NilError(t, os.Chdir(dir)) t.Cleanup(func() { assert.NilError(t, os.Chdir(workingDirectory)) }) } func TestIsArchive(t *testing.T) { testcases := []struct { doc string header []byte expected bool }{ { doc: "nil is not a valid header", header: nil, expected: false, }, { doc: "invalid header bytes", header: []byte{0x00, 0x01, 0x02}, expected: false, }, { doc: "header for bzip2 archive", header: []byte{0x42, 0x5A, 0x68}, expected: true, }, { doc: "header for 7zip archive is not supported", header: []byte{0x50, 0x4b, 0x03, 0x04}, expected: false, }, } for _, testcase := range testcases { assert.Check(t, is.Equal(testcase.expected, IsArchive(testcase.header)), testcase.doc) } } func TestDetectArchiveReader(t *testing.T) { testcases := []struct { file string desc string expected bool }{ { file: "../testdata/tar.test", desc: "tar file without pax headers", expected: true, }, { file: "../testdata/gittar.test", desc: "tar file with pax headers", expected: true, }, { file: "../testdata/Dockerfile.test", desc: "not a tar file", expected: false, }, } for _, testcase := range testcases { content, err := os.Open(testcase.file) assert.NilError(t, err) defer content.Close() _, isArchive, err := DetectArchiveReader(content) assert.NilError(t, err) assert.Check(t, is.Equal(testcase.expected, isArchive), testcase.file) } } func mustPatternMatcher(t *testing.T, patterns []string) *patternmatcher.PatternMatcher { t.Helper() pm, err := patternmatcher.New(patterns) if err != nil { t.Fatal("failed to construct pattern matcher: ", err) } return pm } func TestWildcardMatches(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"*"}), "fileutils.go") if !match { t.Errorf("failed to get a wildcard match, got %v", match) } } // A simple pattern match should return true. func TestPatternMatches(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go"}), "fileutils.go") if !match { t.Errorf("failed to get a match, got %v", match) } } // An exclusion followed by an inclusion should return true. func TestExclusionPatternMatchesPatternBefore(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"!fileutils.go", "*.go"}), "fileutils.go") if !match { t.Errorf("failed to get true match on exclusion pattern, got %v", match) } } // A folder pattern followed by an exception should return false. func TestPatternMatchesFolderExclusions(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs", "!docs/README.md"}), "docs/README.md") if match { t.Errorf("failed to get a false match on exclusion pattern, got %v", match) } } // A folder pattern followed by an exception should return false. func TestPatternMatchesFolderWithSlashExclusions(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs/", "!docs/README.md"}), "docs/README.md") if match { t.Errorf("failed to get a false match on exclusion pattern, got %v", match) } } // A folder pattern followed by an exception should return false. func TestPatternMatchesFolderWildcardExclusions(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs/*", "!docs/README.md"}), "docs/README.md") if match { t.Errorf("failed to get a false match on exclusion pattern, got %v", match) } } // A pattern followed by an exclusion should return false. func TestExclusionPatternMatchesPatternAfter(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go", "!fileutils.go"}), "fileutils.go") if match { t.Errorf("failed to get false match on exclusion pattern, got %v", match) } } // A filename evaluating to . should return false. func TestExclusionPatternMatchesWholeDirectory(t *testing.T) { match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go"}), ".") if match { t.Errorf("failed to get false match on ., got %v", match) } } // Matches with no patterns func TestMatchesWithNoPatterns(t *testing.T) { matches, err := filepathMatches(mustPatternMatcher(t, []string{}), "/any/path/there") if err != nil { t.Fatal(err) } if matches { t.Fatalf("Should not have match anything") } }