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 }