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