github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/image/build/context_test.go (about)

     1  package build
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/docker/docker/pkg/archive"
    14  	"github.com/moby/patternmatcher"
    15  	"gotest.tools/v3/assert"
    16  	is "gotest.tools/v3/assert/cmp"
    17  )
    18  
    19  const dockerfileContents = "FROM busybox"
    20  
    21  func prepareEmpty(t *testing.T) string {
    22  	return ""
    23  }
    24  
    25  func prepareNoFiles(t *testing.T) string {
    26  	return createTestTempDir(t)
    27  }
    28  
    29  func prepareOneFile(t *testing.T) string {
    30  	contextDir := createTestTempDir(t)
    31  	createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
    32  	return contextDir
    33  }
    34  
    35  func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) string, excludes []string) {
    36  	contextDir := prepare(t)
    37  	err := ValidateContextDirectory(contextDir, excludes)
    38  	assert.NilError(t, err)
    39  }
    40  
    41  func TestGetContextFromLocalDirNoDockerfile(t *testing.T) {
    42  	contextDir := createTestTempDir(t)
    43  	_, _, err := GetContextFromLocalDir(contextDir, "")
    44  	assert.ErrorContains(t, err, "Dockerfile")
    45  }
    46  
    47  func TestGetContextFromLocalDirNotExistingDir(t *testing.T) {
    48  	contextDir := createTestTempDir(t)
    49  	fakePath := filepath.Join(contextDir, "fake")
    50  
    51  	_, _, err := GetContextFromLocalDir(fakePath, "")
    52  	assert.ErrorContains(t, err, "fake")
    53  }
    54  
    55  func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) {
    56  	contextDir := createTestTempDir(t)
    57  	fakePath := filepath.Join(contextDir, "fake")
    58  
    59  	_, _, err := GetContextFromLocalDir(contextDir, fakePath)
    60  	assert.ErrorContains(t, err, "fake")
    61  }
    62  
    63  func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) {
    64  	contextDir := createTestTempDir(t)
    65  	createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
    66  
    67  	chdir(t, contextDir)
    68  
    69  	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
    70  	assert.NilError(t, err)
    71  
    72  	assert.Check(t, is.Equal(contextDir, absContextDir))
    73  	assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile))
    74  }
    75  
    76  func TestGetContextFromLocalDirWithDockerfile(t *testing.T) {
    77  	contextDir := createTestTempDir(t)
    78  	createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
    79  
    80  	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
    81  	assert.NilError(t, err)
    82  
    83  	assert.Check(t, is.Equal(contextDir, absContextDir))
    84  	assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile))
    85  }
    86  
    87  func TestGetContextFromLocalDirLocalFile(t *testing.T) {
    88  	contextDir := createTestTempDir(t)
    89  	createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
    90  	testFilename := createTestTempFile(t, contextDir, "tmpTest", "test")
    91  
    92  	absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "")
    93  
    94  	if err == nil {
    95  		t.Fatalf("Error should not be nil")
    96  	}
    97  
    98  	if absContextDir != "" {
    99  		t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
   100  	}
   101  
   102  	if relDockerfile != "" {
   103  		t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
   104  	}
   105  }
   106  
   107  func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) {
   108  	contextDir := createTestTempDir(t)
   109  	chdir(t, contextDir)
   110  
   111  	createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
   112  
   113  	absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName)
   114  	assert.NilError(t, err)
   115  
   116  	assert.Check(t, is.Equal(contextDir, absContextDir))
   117  	assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile))
   118  }
   119  
   120  func TestGetContextFromReaderString(t *testing.T) {
   121  	tarArchive, relDockerfile, err := GetContextFromReader(io.NopCloser(strings.NewReader(dockerfileContents)), "")
   122  	if err != nil {
   123  		t.Fatalf("Error when executing GetContextFromReader: %s", err)
   124  	}
   125  
   126  	tarReader := tar.NewReader(tarArchive)
   127  
   128  	_, err = tarReader.Next()
   129  
   130  	if err != nil {
   131  		t.Fatalf("Error when reading tar archive: %s", err)
   132  	}
   133  
   134  	buff := new(bytes.Buffer)
   135  	buff.ReadFrom(tarReader)
   136  	contents := buff.String()
   137  
   138  	_, err = tarReader.Next()
   139  
   140  	if err != io.EOF {
   141  		t.Fatalf("Tar stream too long: %s", err)
   142  	}
   143  
   144  	assert.NilError(t, tarArchive.Close())
   145  
   146  	if dockerfileContents != contents {
   147  		t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents)
   148  	}
   149  
   150  	if relDockerfile != DefaultDockerfileName {
   151  		t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile)
   152  	}
   153  }
   154  
   155  func TestGetContextFromReaderTar(t *testing.T) {
   156  	contextDir := createTestTempDir(t)
   157  	createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
   158  
   159  	tarStream, err := archive.Tar(contextDir, archive.Uncompressed)
   160  	assert.NilError(t, err)
   161  
   162  	tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName)
   163  	assert.NilError(t, err)
   164  
   165  	tarReader := tar.NewReader(tarArchive)
   166  
   167  	header, err := tarReader.Next()
   168  	assert.NilError(t, err)
   169  
   170  	if header.Name != DefaultDockerfileName {
   171  		t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name)
   172  	}
   173  
   174  	buff := new(bytes.Buffer)
   175  	buff.ReadFrom(tarReader)
   176  	contents := buff.String()
   177  
   178  	_, err = tarReader.Next()
   179  
   180  	if err != io.EOF {
   181  		t.Fatalf("Tar stream too long: %s", err)
   182  	}
   183  
   184  	assert.NilError(t, tarArchive.Close())
   185  
   186  	if dockerfileContents != contents {
   187  		t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents)
   188  	}
   189  
   190  	if relDockerfile != DefaultDockerfileName {
   191  		t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile)
   192  	}
   193  }
   194  
   195  func TestValidateContextDirectoryEmptyContext(t *testing.T) {
   196  	// This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81.
   197  	// The test will ultimately end up calling filepath.Abs(""). On Windows,
   198  	// golang will error. On Linux, golang will return /. Due to there being
   199  	// drive letters on Windows, this is probably the correct behaviour for
   200  	// Windows.
   201  	if runtime.GOOS == "windows" {
   202  		t.Skip("Invalid test on Windows")
   203  	}
   204  	testValidateContextDirectory(t, prepareEmpty, []string{})
   205  }
   206  
   207  func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) {
   208  	testValidateContextDirectory(t, prepareNoFiles, []string{})
   209  }
   210  
   211  func TestValidateContextDirectoryWithOneFile(t *testing.T) {
   212  	testValidateContextDirectory(t, prepareOneFile, []string{})
   213  }
   214  
   215  func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) {
   216  	testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName})
   217  }
   218  
   219  // createTestTempDir creates a temporary directory for testing. It returns the
   220  // created path. When an error occurs, it terminates the test.
   221  func createTestTempDir(t *testing.T) string {
   222  	t.Helper()
   223  	path := t.TempDir()
   224  
   225  	// Eval Symlinks is needed to account for macOS TMP using symlinks
   226  	path, err := filepath.EvalSymlinks(path)
   227  	assert.NilError(t, err)
   228  	return path
   229  }
   230  
   231  // createTestTempFile creates a temporary file within dir with specific contents and permissions.
   232  // When an error occurs, it terminates the test
   233  func createTestTempFile(t *testing.T, dir, filename, contents string) string {
   234  	t.Helper()
   235  	filePath := filepath.Join(dir, filename)
   236  	err := os.WriteFile(filePath, []byte(contents), 0o777)
   237  	assert.NilError(t, err)
   238  	return filePath
   239  }
   240  
   241  // chdir changes current working directory to dir.
   242  // It returns a function which changes working directory back to the previous one.
   243  // This function is meant to be executed as a deferred call.
   244  // When an error occurs, it terminates the test.
   245  func chdir(t *testing.T, dir string) {
   246  	workingDirectory, err := os.Getwd()
   247  	assert.NilError(t, err)
   248  	assert.NilError(t, os.Chdir(dir))
   249  	t.Cleanup(func() {
   250  		assert.NilError(t, os.Chdir(workingDirectory))
   251  	})
   252  }
   253  
   254  func TestIsArchive(t *testing.T) {
   255  	testcases := []struct {
   256  		doc      string
   257  		header   []byte
   258  		expected bool
   259  	}{
   260  		{
   261  			doc:      "nil is not a valid header",
   262  			header:   nil,
   263  			expected: false,
   264  		},
   265  		{
   266  			doc:      "invalid header bytes",
   267  			header:   []byte{0x00, 0x01, 0x02},
   268  			expected: false,
   269  		},
   270  		{
   271  			doc:      "header for bzip2 archive",
   272  			header:   []byte{0x42, 0x5A, 0x68},
   273  			expected: true,
   274  		},
   275  		{
   276  			doc:      "header for 7zip archive is not supported",
   277  			header:   []byte{0x50, 0x4b, 0x03, 0x04},
   278  			expected: false,
   279  		},
   280  	}
   281  	for _, testcase := range testcases {
   282  		assert.Check(t, is.Equal(testcase.expected, IsArchive(testcase.header)), testcase.doc)
   283  	}
   284  }
   285  
   286  func TestDetectArchiveReader(t *testing.T) {
   287  	testcases := []struct {
   288  		file     string
   289  		desc     string
   290  		expected bool
   291  	}{
   292  		{
   293  			file:     "../testdata/tar.test",
   294  			desc:     "tar file without pax headers",
   295  			expected: true,
   296  		},
   297  		{
   298  			file:     "../testdata/gittar.test",
   299  			desc:     "tar file with pax headers",
   300  			expected: true,
   301  		},
   302  		{
   303  			file:     "../testdata/Dockerfile.test",
   304  			desc:     "not a tar file",
   305  			expected: false,
   306  		},
   307  	}
   308  	for _, testcase := range testcases {
   309  		content, err := os.Open(testcase.file)
   310  		assert.NilError(t, err)
   311  		defer content.Close()
   312  
   313  		_, isArchive, err := DetectArchiveReader(content)
   314  		assert.NilError(t, err)
   315  		assert.Check(t, is.Equal(testcase.expected, isArchive), testcase.file)
   316  	}
   317  }
   318  
   319  func mustPatternMatcher(t *testing.T, patterns []string) *patternmatcher.PatternMatcher {
   320  	t.Helper()
   321  	pm, err := patternmatcher.New(patterns)
   322  	if err != nil {
   323  		t.Fatal("failed to construct pattern matcher: ", err)
   324  	}
   325  	return pm
   326  }
   327  
   328  func TestWildcardMatches(t *testing.T) {
   329  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"*"}), "fileutils.go")
   330  	if !match {
   331  		t.Errorf("failed to get a wildcard match, got %v", match)
   332  	}
   333  }
   334  
   335  // A simple pattern match should return true.
   336  func TestPatternMatches(t *testing.T) {
   337  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go"}), "fileutils.go")
   338  	if !match {
   339  		t.Errorf("failed to get a match, got %v", match)
   340  	}
   341  }
   342  
   343  // An exclusion followed by an inclusion should return true.
   344  func TestExclusionPatternMatchesPatternBefore(t *testing.T) {
   345  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"!fileutils.go", "*.go"}), "fileutils.go")
   346  	if !match {
   347  		t.Errorf("failed to get true match on exclusion pattern, got %v", match)
   348  	}
   349  }
   350  
   351  // A folder pattern followed by an exception should return false.
   352  func TestPatternMatchesFolderExclusions(t *testing.T) {
   353  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs", "!docs/README.md"}), "docs/README.md")
   354  	if match {
   355  		t.Errorf("failed to get a false match on exclusion pattern, got %v", match)
   356  	}
   357  }
   358  
   359  // A folder pattern followed by an exception should return false.
   360  func TestPatternMatchesFolderWithSlashExclusions(t *testing.T) {
   361  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs/", "!docs/README.md"}), "docs/README.md")
   362  	if match {
   363  		t.Errorf("failed to get a false match on exclusion pattern, got %v", match)
   364  	}
   365  }
   366  
   367  // A folder pattern followed by an exception should return false.
   368  func TestPatternMatchesFolderWildcardExclusions(t *testing.T) {
   369  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs/*", "!docs/README.md"}), "docs/README.md")
   370  	if match {
   371  		t.Errorf("failed to get a false match on exclusion pattern, got %v", match)
   372  	}
   373  }
   374  
   375  // A pattern followed by an exclusion should return false.
   376  func TestExclusionPatternMatchesPatternAfter(t *testing.T) {
   377  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go", "!fileutils.go"}), "fileutils.go")
   378  	if match {
   379  		t.Errorf("failed to get false match on exclusion pattern, got %v", match)
   380  	}
   381  }
   382  
   383  // A filename evaluating to . should return false.
   384  func TestExclusionPatternMatchesWholeDirectory(t *testing.T) {
   385  	match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go"}), ".")
   386  	if match {
   387  		t.Errorf("failed to get false match on ., got %v", match)
   388  	}
   389  }
   390  
   391  // Matches with no patterns
   392  func TestMatchesWithNoPatterns(t *testing.T) {
   393  	matches, err := filepathMatches(mustPatternMatcher(t, []string{}), "/any/path/there")
   394  	if err != nil {
   395  		t.Fatal(err)
   396  	}
   397  	if matches {
   398  		t.Fatalf("Should not have match anything")
   399  	}
   400  }