gopkg.in/docker/docker.v20@v20.10.27/integration/build/build_test.go (about) 1 package build // import "github.com/docker/docker/integration/build" 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "context" 7 "encoding/json" 8 "io" 9 "os" 10 "strings" 11 "testing" 12 13 "github.com/docker/docker/api/types" 14 "github.com/docker/docker/api/types/filters" 15 "github.com/docker/docker/api/types/versions" 16 "github.com/docker/docker/errdefs" 17 "github.com/docker/docker/pkg/jsonmessage" 18 "github.com/docker/docker/testutil/fakecontext" 19 "gotest.tools/v3/assert" 20 is "gotest.tools/v3/assert/cmp" 21 "gotest.tools/v3/skip" 22 ) 23 24 func TestBuildWithRemoveAndForceRemove(t *testing.T) { 25 defer setupTest(t)() 26 27 cases := []struct { 28 name string 29 dockerfile string 30 numberOfIntermediateContainers int 31 rm bool 32 forceRm bool 33 }{ 34 { 35 name: "successful build with no removal", 36 dockerfile: `FROM busybox 37 RUN exit 0 38 RUN exit 0`, 39 numberOfIntermediateContainers: 2, 40 rm: false, 41 forceRm: false, 42 }, 43 { 44 name: "successful build with remove", 45 dockerfile: `FROM busybox 46 RUN exit 0 47 RUN exit 0`, 48 numberOfIntermediateContainers: 0, 49 rm: true, 50 forceRm: false, 51 }, 52 { 53 name: "successful build with remove and force remove", 54 dockerfile: `FROM busybox 55 RUN exit 0 56 RUN exit 0`, 57 numberOfIntermediateContainers: 0, 58 rm: true, 59 forceRm: true, 60 }, 61 { 62 name: "failed build with no removal", 63 dockerfile: `FROM busybox 64 RUN exit 0 65 RUN exit 1`, 66 numberOfIntermediateContainers: 2, 67 rm: false, 68 forceRm: false, 69 }, 70 { 71 name: "failed build with remove", 72 dockerfile: `FROM busybox 73 RUN exit 0 74 RUN exit 1`, 75 numberOfIntermediateContainers: 1, 76 rm: true, 77 forceRm: false, 78 }, 79 { 80 name: "failed build with remove and force remove", 81 dockerfile: `FROM busybox 82 RUN exit 0 83 RUN exit 1`, 84 numberOfIntermediateContainers: 0, 85 rm: true, 86 forceRm: true, 87 }, 88 } 89 90 client := testEnv.APIClient() 91 ctx := context.Background() 92 for _, c := range cases { 93 c := c 94 t.Run(c.name, func(t *testing.T) { 95 t.Parallel() 96 dockerfile := []byte(c.dockerfile) 97 98 buff := bytes.NewBuffer(nil) 99 tw := tar.NewWriter(buff) 100 assert.NilError(t, tw.WriteHeader(&tar.Header{ 101 Name: "Dockerfile", 102 Size: int64(len(dockerfile)), 103 })) 104 _, err := tw.Write(dockerfile) 105 assert.NilError(t, err) 106 assert.NilError(t, tw.Close()) 107 resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true}) 108 assert.NilError(t, err) 109 defer resp.Body.Close() 110 filter, err := buildContainerIdsFilter(resp.Body) 111 assert.NilError(t, err) 112 remainingContainers, err := client.ContainerList(ctx, types.ContainerListOptions{Filters: filter, All: true}) 113 assert.NilError(t, err) 114 assert.Equal(t, c.numberOfIntermediateContainers, len(remainingContainers), "Expected %v remaining intermediate containers, got %v", c.numberOfIntermediateContainers, len(remainingContainers)) 115 }) 116 } 117 } 118 119 func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) { 120 const intermediateContainerPrefix = " ---> Running in " 121 filter := filters.NewArgs() 122 123 dec := json.NewDecoder(buildOutput) 124 for { 125 m := jsonmessage.JSONMessage{} 126 err := dec.Decode(&m) 127 if err == io.EOF { 128 return filter, nil 129 } 130 if err != nil { 131 return filter, err 132 } 133 if ix := strings.Index(m.Stream, intermediateContainerPrefix); ix != -1 { 134 filter.Add("id", strings.TrimSpace(m.Stream[ix+len(intermediateContainerPrefix):])) 135 } 136 } 137 } 138 139 // TestBuildMultiStageCopy verifies that copying between stages works correctly. 140 // 141 // Regression test for docker/for-win#4349, ENGCORE-935, where creating the target 142 // directory failed on Windows, because `os.MkdirAll()` was called with a volume 143 // GUID path (\\?\Volume{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\newdir\hello}), 144 // which currently isn't supported by Golang. 145 func TestBuildMultiStageCopy(t *testing.T) { 146 ctx := context.Background() 147 148 dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name()) 149 assert.NilError(t, err) 150 151 source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) 152 defer source.Close() 153 154 apiclient := testEnv.APIClient() 155 156 for _, target := range []string{"copy_to_root", "copy_to_newdir", "copy_to_newdir_nested", "copy_to_existingdir", "copy_to_newsubdir"} { 157 t.Run(target, func(t *testing.T) { 158 imgName := strings.ToLower(t.Name()) 159 160 resp, err := apiclient.ImageBuild( 161 ctx, 162 source.AsTarReader(t), 163 types.ImageBuildOptions{ 164 Remove: true, 165 ForceRemove: true, 166 Target: target, 167 Tags: []string{imgName}, 168 }, 169 ) 170 assert.NilError(t, err) 171 172 out := bytes.NewBuffer(nil) 173 _, err = io.Copy(out, resp.Body) 174 _ = resp.Body.Close() 175 if err != nil { 176 t.Log(out) 177 } 178 assert.NilError(t, err) 179 180 // verify the image was successfully built 181 _, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) 182 if err != nil { 183 t.Log(out) 184 } 185 assert.NilError(t, err) 186 }) 187 } 188 } 189 190 func TestBuildMultiStageParentConfig(t *testing.T) { 191 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.35"), "broken in earlier versions") 192 dockerfile := ` 193 FROM busybox AS stage0 194 ENV WHO=parent 195 WORKDIR /foo 196 197 FROM stage0 198 ENV WHO=sibling1 199 WORKDIR sub1 200 201 FROM stage0 202 WORKDIR sub2 203 ` 204 ctx := context.Background() 205 source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) 206 defer source.Close() 207 208 apiclient := testEnv.APIClient() 209 imgName := strings.ToLower(t.Name()) 210 resp, err := apiclient.ImageBuild(ctx, 211 source.AsTarReader(t), 212 types.ImageBuildOptions{ 213 Remove: true, 214 ForceRemove: true, 215 Tags: []string{imgName}, 216 }) 217 assert.NilError(t, err) 218 _, err = io.Copy(io.Discard, resp.Body) 219 resp.Body.Close() 220 assert.NilError(t, err) 221 222 image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) 223 assert.NilError(t, err) 224 225 expected := "/foo/sub2" 226 if testEnv.DaemonInfo.OSType == "windows" { 227 expected = `C:\foo\sub2` 228 } 229 assert.Check(t, is.Equal(expected, image.Config.WorkingDir)) 230 assert.Check(t, is.Contains(image.Config.Env, "WHO=parent")) 231 } 232 233 // Test cases in #36996 234 func TestBuildLabelWithTargets(t *testing.T) { 235 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.38"), "test added after 1.38") 236 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 237 imgName := strings.ToLower(t.Name() + "-a") 238 testLabels := map[string]string{ 239 "foo": "bar", 240 "dead": "beef", 241 } 242 243 dockerfile := ` 244 FROM busybox AS target-a 245 CMD ["/dev"] 246 LABEL label-a=inline-a 247 FROM busybox AS target-b 248 CMD ["/dist"] 249 LABEL label-b=inline-b 250 ` 251 252 ctx := context.Background() 253 source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) 254 defer source.Close() 255 256 apiclient := testEnv.APIClient() 257 // For `target-a` build 258 resp, err := apiclient.ImageBuild(ctx, 259 source.AsTarReader(t), 260 types.ImageBuildOptions{ 261 Remove: true, 262 ForceRemove: true, 263 Tags: []string{imgName}, 264 Labels: testLabels, 265 Target: "target-a", 266 }) 267 assert.NilError(t, err) 268 _, err = io.Copy(io.Discard, resp.Body) 269 resp.Body.Close() 270 assert.NilError(t, err) 271 272 image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) 273 assert.NilError(t, err) 274 275 testLabels["label-a"] = "inline-a" 276 for k, v := range testLabels { 277 x, ok := image.Config.Labels[k] 278 assert.Assert(t, ok) 279 assert.Assert(t, x == v) 280 } 281 282 // For `target-b` build 283 imgName = strings.ToLower(t.Name() + "-b") 284 delete(testLabels, "label-a") 285 resp, err = apiclient.ImageBuild(ctx, 286 source.AsTarReader(t), 287 types.ImageBuildOptions{ 288 Remove: true, 289 ForceRemove: true, 290 Tags: []string{imgName}, 291 Labels: testLabels, 292 Target: "target-b", 293 }) 294 assert.NilError(t, err) 295 _, err = io.Copy(io.Discard, resp.Body) 296 resp.Body.Close() 297 assert.NilError(t, err) 298 299 image, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) 300 assert.NilError(t, err) 301 302 testLabels["label-b"] = "inline-b" 303 for k, v := range testLabels { 304 x, ok := image.Config.Labels[k] 305 assert.Assert(t, ok) 306 assert.Assert(t, x == v) 307 } 308 } 309 310 func TestBuildWithEmptyLayers(t *testing.T) { 311 dockerfile := ` 312 FROM busybox 313 COPY 1/ /target/ 314 COPY 2/ /target/ 315 COPY 3/ /target/ 316 ` 317 ctx := context.Background() 318 source := fakecontext.New(t, "", 319 fakecontext.WithDockerfile(dockerfile), 320 fakecontext.WithFile("1/a", "asdf"), 321 fakecontext.WithFile("2/a", "asdf"), 322 fakecontext.WithFile("3/a", "asdf")) 323 defer source.Close() 324 325 apiclient := testEnv.APIClient() 326 resp, err := apiclient.ImageBuild(ctx, 327 source.AsTarReader(t), 328 types.ImageBuildOptions{ 329 Remove: true, 330 ForceRemove: true, 331 }) 332 assert.NilError(t, err) 333 _, err = io.Copy(io.Discard, resp.Body) 334 resp.Body.Close() 335 assert.NilError(t, err) 336 } 337 338 // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to 339 // multiple subsequent stages 340 // #35652 341 func TestBuildMultiStageOnBuild(t *testing.T) { 342 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.33"), "broken in earlier versions") 343 defer setupTest(t)() 344 // test both metadata and layer based commands as they may be implemented differently 345 dockerfile := `FROM busybox AS stage1 346 ONBUILD RUN echo 'foo' >somefile 347 ONBUILD ENV bar=baz 348 349 FROM stage1 350 # fails if ONBUILD RUN fails 351 RUN cat somefile 352 353 FROM stage1 354 RUN cat somefile` 355 356 ctx := context.Background() 357 source := fakecontext.New(t, "", 358 fakecontext.WithDockerfile(dockerfile)) 359 defer source.Close() 360 361 apiclient := testEnv.APIClient() 362 resp, err := apiclient.ImageBuild(ctx, 363 source.AsTarReader(t), 364 types.ImageBuildOptions{ 365 Remove: true, 366 ForceRemove: true, 367 }) 368 369 out := bytes.NewBuffer(nil) 370 assert.NilError(t, err) 371 _, err = io.Copy(out, resp.Body) 372 resp.Body.Close() 373 assert.NilError(t, err) 374 375 assert.Check(t, is.Contains(out.String(), "Successfully built")) 376 377 imageIDs, err := getImageIDsFromBuild(out.Bytes()) 378 assert.NilError(t, err) 379 assert.Assert(t, is.Equal(3, len(imageIDs))) 380 381 image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2]) 382 assert.NilError(t, err) 383 assert.Check(t, is.Contains(image.Config.Env, "bar=baz")) 384 } 385 386 // #35403 #36122 387 func TestBuildUncleanTarFilenames(t *testing.T) { 388 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions") 389 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 390 391 ctx := context.TODO() 392 defer setupTest(t)() 393 394 dockerfile := `FROM scratch 395 COPY foo / 396 FROM scratch 397 COPY bar /` 398 399 buf := bytes.NewBuffer(nil) 400 w := tar.NewWriter(buf) 401 writeTarRecord(t, w, "Dockerfile", dockerfile) 402 writeTarRecord(t, w, "../foo", "foocontents0") 403 writeTarRecord(t, w, "/bar", "barcontents0") 404 err := w.Close() 405 assert.NilError(t, err) 406 407 apiclient := testEnv.APIClient() 408 resp, err := apiclient.ImageBuild(ctx, 409 buf, 410 types.ImageBuildOptions{ 411 Remove: true, 412 ForceRemove: true, 413 }) 414 415 out := bytes.NewBuffer(nil) 416 assert.NilError(t, err) 417 _, err = io.Copy(out, resp.Body) 418 resp.Body.Close() 419 assert.NilError(t, err) 420 421 // repeat with changed data should not cause cache hits 422 423 buf = bytes.NewBuffer(nil) 424 w = tar.NewWriter(buf) 425 writeTarRecord(t, w, "Dockerfile", dockerfile) 426 writeTarRecord(t, w, "../foo", "foocontents1") 427 writeTarRecord(t, w, "/bar", "barcontents1") 428 err = w.Close() 429 assert.NilError(t, err) 430 431 resp, err = apiclient.ImageBuild(ctx, 432 buf, 433 types.ImageBuildOptions{ 434 Remove: true, 435 ForceRemove: true, 436 }) 437 438 out = bytes.NewBuffer(nil) 439 assert.NilError(t, err) 440 _, err = io.Copy(out, resp.Body) 441 resp.Body.Close() 442 assert.NilError(t, err) 443 assert.Assert(t, !strings.Contains(out.String(), "Using cache")) 444 } 445 446 // docker/for-linux#135 447 // #35641 448 func TestBuildMultiStageLayerLeak(t *testing.T) { 449 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions") 450 ctx := context.TODO() 451 defer setupTest(t)() 452 453 // all commands need to match until COPY 454 dockerfile := `FROM busybox 455 WORKDIR /foo 456 COPY foo . 457 FROM busybox 458 WORKDIR /foo 459 COPY bar . 460 RUN [ -f bar ] 461 RUN [ ! -f foo ] 462 ` 463 464 source := fakecontext.New(t, "", 465 fakecontext.WithFile("foo", "0"), 466 fakecontext.WithFile("bar", "1"), 467 fakecontext.WithDockerfile(dockerfile)) 468 defer source.Close() 469 470 apiclient := testEnv.APIClient() 471 resp, err := apiclient.ImageBuild(ctx, 472 source.AsTarReader(t), 473 types.ImageBuildOptions{ 474 Remove: true, 475 ForceRemove: true, 476 }) 477 478 out := bytes.NewBuffer(nil) 479 assert.NilError(t, err) 480 _, err = io.Copy(out, resp.Body) 481 resp.Body.Close() 482 assert.NilError(t, err) 483 484 assert.Check(t, is.Contains(out.String(), "Successfully built")) 485 } 486 487 // #37581 488 // #40444 (Windows Containers only) 489 func TestBuildWithHugeFile(t *testing.T) { 490 ctx := context.TODO() 491 defer setupTest(t)() 492 493 dockerfile := `FROM busybox 494 ` 495 496 if testEnv.DaemonInfo.OSType == "windows" { 497 dockerfile += `# create a file with size of 8GB 498 RUN powershell "fsutil.exe file createnew bigfile.txt 8589934592 ; dir bigfile.txt"` 499 } else { 500 dockerfile += `# create a sparse file with size over 8GB 501 RUN for g in $(seq 0 8); do dd if=/dev/urandom of=rnd bs=1K count=1 seek=$((1024*1024*g)) status=none; done && \ 502 ls -la rnd && du -sk rnd` 503 } 504 505 buf := bytes.NewBuffer(nil) 506 w := tar.NewWriter(buf) 507 writeTarRecord(t, w, "Dockerfile", dockerfile) 508 err := w.Close() 509 assert.NilError(t, err) 510 511 apiclient := testEnv.APIClient() 512 resp, err := apiclient.ImageBuild(ctx, 513 buf, 514 types.ImageBuildOptions{ 515 Remove: true, 516 ForceRemove: true, 517 }) 518 519 out := bytes.NewBuffer(nil) 520 assert.NilError(t, err) 521 _, err = io.Copy(out, resp.Body) 522 resp.Body.Close() 523 assert.NilError(t, err) 524 assert.Check(t, is.Contains(out.String(), "Successfully built")) 525 } 526 527 func TestBuildWithEmptyDockerfile(t *testing.T) { 528 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions") 529 ctx := context.TODO() 530 defer setupTest(t)() 531 532 tests := []struct { 533 name string 534 dockerfile string 535 expectedErr string 536 }{ 537 { 538 name: "empty-dockerfile", 539 dockerfile: "", 540 expectedErr: "cannot be empty", 541 }, 542 { 543 name: "empty-lines-dockerfile", 544 dockerfile: ` 545 546 547 548 `, 549 expectedErr: "file with no instructions", 550 }, 551 { 552 name: "comment-only-dockerfile", 553 dockerfile: `# this is a comment`, 554 expectedErr: "file with no instructions", 555 }, 556 } 557 558 apiclient := testEnv.APIClient() 559 560 for _, tc := range tests { 561 tc := tc 562 t.Run(tc.name, func(t *testing.T) { 563 t.Parallel() 564 565 buf := bytes.NewBuffer(nil) 566 w := tar.NewWriter(buf) 567 writeTarRecord(t, w, "Dockerfile", tc.dockerfile) 568 err := w.Close() 569 assert.NilError(t, err) 570 571 _, err = apiclient.ImageBuild(ctx, 572 buf, 573 types.ImageBuildOptions{ 574 Remove: true, 575 ForceRemove: true, 576 }) 577 578 assert.Check(t, is.Contains(err.Error(), tc.expectedErr)) 579 }) 580 } 581 } 582 583 func TestBuildPreserveOwnership(t *testing.T) { 584 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 585 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions") 586 587 ctx := context.Background() 588 589 dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name()) 590 assert.NilError(t, err) 591 592 source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) 593 defer source.Close() 594 595 apiclient := testEnv.APIClient() 596 597 for _, target := range []string{"copy_from", "copy_from_chowned"} { 598 t.Run(target, func(t *testing.T) { 599 resp, err := apiclient.ImageBuild( 600 ctx, 601 source.AsTarReader(t), 602 types.ImageBuildOptions{ 603 Remove: true, 604 ForceRemove: true, 605 Target: target, 606 }, 607 ) 608 assert.NilError(t, err) 609 610 out := bytes.NewBuffer(nil) 611 _, err = io.Copy(out, resp.Body) 612 _ = resp.Body.Close() 613 if err != nil { 614 t.Log(out) 615 } 616 assert.NilError(t, err) 617 }) 618 } 619 } 620 621 func TestBuildPlatformInvalid(t *testing.T) { 622 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions") 623 624 ctx := context.Background() 625 defer setupTest(t)() 626 627 dockerfile := `FROM busybox 628 ` 629 630 buf := bytes.NewBuffer(nil) 631 w := tar.NewWriter(buf) 632 writeTarRecord(t, w, "Dockerfile", dockerfile) 633 err := w.Close() 634 assert.NilError(t, err) 635 636 apiclient := testEnv.APIClient() 637 _, err = apiclient.ImageBuild(ctx, 638 buf, 639 types.ImageBuildOptions{ 640 Remove: true, 641 ForceRemove: true, 642 Platform: "foobar", 643 }) 644 645 assert.Assert(t, err != nil) 646 assert.ErrorContains(t, err, "unknown operating system or architecture") 647 assert.Assert(t, errdefs.IsInvalidParameter(err)) 648 } 649 650 func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) { 651 err := w.WriteHeader(&tar.Header{ 652 Name: fn, 653 Mode: 0600, 654 Size: int64(len(contents)), 655 Typeflag: '0', 656 }) 657 assert.NilError(t, err) 658 _, err = w.Write([]byte(contents)) 659 assert.NilError(t, err) 660 } 661 662 type buildLine struct { 663 Stream string 664 Aux struct { 665 ID string 666 } 667 } 668 669 func getImageIDsFromBuild(output []byte) ([]string, error) { 670 var ids []string 671 for _, line := range bytes.Split(output, []byte("\n")) { 672 if len(line) == 0 { 673 continue 674 } 675 entry := buildLine{} 676 if err := json.Unmarshal(line, &entry); err != nil { 677 return nil, err 678 } 679 if entry.Aux.ID != "" { 680 ids = append(ids, entry.Aux.ID) 681 } 682 } 683 return ids, nil 684 }