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