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 }