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