github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/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 "io/ioutil" 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 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 26 defer setupTest(t)() 27 28 cases := []struct { 29 name string 30 dockerfile string 31 numberOfIntermediateContainers int 32 rm bool 33 forceRm bool 34 }{ 35 { 36 name: "successful build with no removal", 37 dockerfile: `FROM busybox 38 RUN exit 0 39 RUN exit 0`, 40 numberOfIntermediateContainers: 2, 41 rm: false, 42 forceRm: false, 43 }, 44 { 45 name: "successful build with remove", 46 dockerfile: `FROM busybox 47 RUN exit 0 48 RUN exit 0`, 49 numberOfIntermediateContainers: 0, 50 rm: true, 51 forceRm: false, 52 }, 53 { 54 name: "successful build with remove and force remove", 55 dockerfile: `FROM busybox 56 RUN exit 0 57 RUN exit 0`, 58 numberOfIntermediateContainers: 0, 59 rm: true, 60 forceRm: true, 61 }, 62 { 63 name: "failed build with no removal", 64 dockerfile: `FROM busybox 65 RUN exit 0 66 RUN exit 1`, 67 numberOfIntermediateContainers: 2, 68 rm: false, 69 forceRm: false, 70 }, 71 { 72 name: "failed build with remove", 73 dockerfile: `FROM busybox 74 RUN exit 0 75 RUN exit 1`, 76 numberOfIntermediateContainers: 1, 77 rm: true, 78 forceRm: false, 79 }, 80 { 81 name: "failed build with remove and force remove", 82 dockerfile: `FROM busybox 83 RUN exit 0 84 RUN exit 1`, 85 numberOfIntermediateContainers: 0, 86 rm: true, 87 forceRm: true, 88 }, 89 } 90 91 client := testEnv.APIClient() 92 ctx := context.Background() 93 for _, c := range cases { 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 := ioutil.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 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 193 dockerfile := ` 194 FROM busybox AS stage0 195 ENV WHO=parent 196 WORKDIR /foo 197 198 FROM stage0 199 ENV WHO=sibling1 200 WORKDIR sub1 201 202 FROM stage0 203 WORKDIR sub2 204 ` 205 ctx := context.Background() 206 source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) 207 defer source.Close() 208 209 apiclient := testEnv.APIClient() 210 imgName := strings.ToLower(t.Name()) 211 resp, err := apiclient.ImageBuild(ctx, 212 source.AsTarReader(t), 213 types.ImageBuildOptions{ 214 Remove: true, 215 ForceRemove: true, 216 Tags: []string{imgName}, 217 }) 218 assert.NilError(t, err) 219 _, err = io.Copy(ioutil.Discard, resp.Body) 220 resp.Body.Close() 221 assert.NilError(t, err) 222 223 image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) 224 assert.NilError(t, err) 225 226 expected := "/foo/sub2" 227 if testEnv.DaemonInfo.OSType == "windows" { 228 expected = `C:\foo\sub2` 229 } 230 assert.Check(t, is.Equal(expected, image.Config.WorkingDir)) 231 assert.Check(t, is.Contains(image.Config.Env, "WHO=parent")) 232 } 233 234 // Test cases in #36996 235 func TestBuildLabelWithTargets(t *testing.T) { 236 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.38"), "test added after 1.38") 237 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 238 imgName := strings.ToLower(t.Name() + "-a") 239 testLabels := map[string]string{ 240 "foo": "bar", 241 "dead": "beef", 242 } 243 244 dockerfile := ` 245 FROM busybox AS target-a 246 CMD ["/dev"] 247 LABEL label-a=inline-a 248 FROM busybox AS target-b 249 CMD ["/dist"] 250 LABEL label-b=inline-b 251 ` 252 253 ctx := context.Background() 254 source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) 255 defer source.Close() 256 257 apiclient := testEnv.APIClient() 258 // For `target-a` build 259 resp, err := apiclient.ImageBuild(ctx, 260 source.AsTarReader(t), 261 types.ImageBuildOptions{ 262 Remove: true, 263 ForceRemove: true, 264 Tags: []string{imgName}, 265 Labels: testLabels, 266 Target: "target-a", 267 }) 268 assert.NilError(t, err) 269 _, err = io.Copy(ioutil.Discard, resp.Body) 270 resp.Body.Close() 271 assert.NilError(t, err) 272 273 image, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) 274 assert.NilError(t, err) 275 276 testLabels["label-a"] = "inline-a" 277 for k, v := range testLabels { 278 x, ok := image.Config.Labels[k] 279 assert.Assert(t, ok) 280 assert.Assert(t, x == v) 281 } 282 283 // For `target-b` build 284 imgName = strings.ToLower(t.Name() + "-b") 285 delete(testLabels, "label-a") 286 resp, err = apiclient.ImageBuild(ctx, 287 source.AsTarReader(t), 288 types.ImageBuildOptions{ 289 Remove: true, 290 ForceRemove: true, 291 Tags: []string{imgName}, 292 Labels: testLabels, 293 Target: "target-b", 294 }) 295 assert.NilError(t, err) 296 _, err = io.Copy(ioutil.Discard, resp.Body) 297 resp.Body.Close() 298 assert.NilError(t, err) 299 300 image, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) 301 assert.NilError(t, err) 302 303 testLabels["label-b"] = "inline-b" 304 for k, v := range testLabels { 305 x, ok := image.Config.Labels[k] 306 assert.Assert(t, ok) 307 assert.Assert(t, x == v) 308 } 309 } 310 311 func TestBuildWithEmptyLayers(t *testing.T) { 312 dockerfile := ` 313 FROM busybox 314 COPY 1/ /target/ 315 COPY 2/ /target/ 316 COPY 3/ /target/ 317 ` 318 ctx := context.Background() 319 source := fakecontext.New(t, "", 320 fakecontext.WithDockerfile(dockerfile), 321 fakecontext.WithFile("1/a", "asdf"), 322 fakecontext.WithFile("2/a", "asdf"), 323 fakecontext.WithFile("3/a", "asdf")) 324 defer source.Close() 325 326 apiclient := testEnv.APIClient() 327 resp, err := apiclient.ImageBuild(ctx, 328 source.AsTarReader(t), 329 types.ImageBuildOptions{ 330 Remove: true, 331 ForceRemove: true, 332 }) 333 assert.NilError(t, err) 334 _, err = io.Copy(ioutil.Discard, resp.Body) 335 resp.Body.Close() 336 assert.NilError(t, err) 337 } 338 339 // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to 340 // multiple subsequent stages 341 // #35652 342 func TestBuildMultiStageOnBuild(t *testing.T) { 343 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.33"), "broken in earlier versions") 344 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 345 defer setupTest(t)() 346 // test both metadata and layer based commands as they may be implemented differently 347 dockerfile := `FROM busybox AS stage1 348 ONBUILD RUN echo 'foo' >somefile 349 ONBUILD ENV bar=baz 350 351 FROM stage1 352 # fails if ONBUILD RUN fails 353 RUN cat somefile 354 355 FROM stage1 356 RUN cat somefile` 357 358 ctx := context.Background() 359 source := fakecontext.New(t, "", 360 fakecontext.WithDockerfile(dockerfile)) 361 defer source.Close() 362 363 apiclient := testEnv.APIClient() 364 resp, err := apiclient.ImageBuild(ctx, 365 source.AsTarReader(t), 366 types.ImageBuildOptions{ 367 Remove: true, 368 ForceRemove: true, 369 }) 370 371 out := bytes.NewBuffer(nil) 372 assert.NilError(t, err) 373 _, err = io.Copy(out, resp.Body) 374 resp.Body.Close() 375 assert.NilError(t, err) 376 377 assert.Check(t, is.Contains(out.String(), "Successfully built")) 378 379 imageIDs, err := getImageIDsFromBuild(out.Bytes()) 380 assert.NilError(t, err) 381 assert.Assert(t, is.Equal(3, len(imageIDs))) 382 383 image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2]) 384 assert.NilError(t, err) 385 assert.Check(t, is.Contains(image.Config.Env, "bar=baz")) 386 } 387 388 // #35403 #36122 389 func TestBuildUncleanTarFilenames(t *testing.T) { 390 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions") 391 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 392 393 ctx := context.TODO() 394 defer setupTest(t)() 395 396 dockerfile := `FROM scratch 397 COPY foo / 398 FROM scratch 399 COPY bar /` 400 401 buf := bytes.NewBuffer(nil) 402 w := tar.NewWriter(buf) 403 writeTarRecord(t, w, "Dockerfile", dockerfile) 404 writeTarRecord(t, w, "../foo", "foocontents0") 405 writeTarRecord(t, w, "/bar", "barcontents0") 406 err := w.Close() 407 assert.NilError(t, err) 408 409 apiclient := testEnv.APIClient() 410 resp, err := apiclient.ImageBuild(ctx, 411 buf, 412 types.ImageBuildOptions{ 413 Remove: true, 414 ForceRemove: true, 415 }) 416 417 out := bytes.NewBuffer(nil) 418 assert.NilError(t, err) 419 _, err = io.Copy(out, resp.Body) 420 resp.Body.Close() 421 assert.NilError(t, err) 422 423 // repeat with changed data should not cause cache hits 424 425 buf = bytes.NewBuffer(nil) 426 w = tar.NewWriter(buf) 427 writeTarRecord(t, w, "Dockerfile", dockerfile) 428 writeTarRecord(t, w, "../foo", "foocontents1") 429 writeTarRecord(t, w, "/bar", "barcontents1") 430 err = w.Close() 431 assert.NilError(t, err) 432 433 resp, err = apiclient.ImageBuild(ctx, 434 buf, 435 types.ImageBuildOptions{ 436 Remove: true, 437 ForceRemove: true, 438 }) 439 440 out = bytes.NewBuffer(nil) 441 assert.NilError(t, err) 442 _, err = io.Copy(out, resp.Body) 443 resp.Body.Close() 444 assert.NilError(t, err) 445 assert.Assert(t, !strings.Contains(out.String(), "Using cache")) 446 } 447 448 // docker/for-linux#135 449 // #35641 450 func TestBuildMultiStageLayerLeak(t *testing.T) { 451 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 452 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions") 453 ctx := context.TODO() 454 defer setupTest(t)() 455 456 // all commands need to match until COPY 457 dockerfile := `FROM busybox 458 WORKDIR /foo 459 COPY foo . 460 FROM busybox 461 WORKDIR /foo 462 COPY bar . 463 RUN [ -f bar ] 464 RUN [ ! -f foo ] 465 ` 466 467 source := fakecontext.New(t, "", 468 fakecontext.WithFile("foo", "0"), 469 fakecontext.WithFile("bar", "1"), 470 fakecontext.WithDockerfile(dockerfile)) 471 defer source.Close() 472 473 apiclient := testEnv.APIClient() 474 resp, err := apiclient.ImageBuild(ctx, 475 source.AsTarReader(t), 476 types.ImageBuildOptions{ 477 Remove: true, 478 ForceRemove: true, 479 }) 480 481 out := bytes.NewBuffer(nil) 482 assert.NilError(t, err) 483 _, err = io.Copy(out, resp.Body) 484 resp.Body.Close() 485 assert.NilError(t, err) 486 487 assert.Check(t, is.Contains(out.String(), "Successfully built")) 488 } 489 490 // #37581 491 func TestBuildWithHugeFile(t *testing.T) { 492 skip.If(t, testEnv.OSType == "windows") 493 ctx := context.TODO() 494 defer setupTest(t)() 495 496 dockerfile := `FROM busybox 497 # create a sparse file with size over 8GB 498 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 && \ 499 ls -la rnd && du -sk rnd` 500 501 buf := bytes.NewBuffer(nil) 502 w := tar.NewWriter(buf) 503 writeTarRecord(t, w, "Dockerfile", dockerfile) 504 err := w.Close() 505 assert.NilError(t, err) 506 507 apiclient := testEnv.APIClient() 508 resp, err := apiclient.ImageBuild(ctx, 509 buf, 510 types.ImageBuildOptions{ 511 Remove: true, 512 ForceRemove: true, 513 }) 514 515 out := bytes.NewBuffer(nil) 516 assert.NilError(t, err) 517 _, err = io.Copy(out, resp.Body) 518 resp.Body.Close() 519 assert.NilError(t, err) 520 assert.Check(t, is.Contains(out.String(), "Successfully built")) 521 } 522 523 func TestBuildWithEmptyDockerfile(t *testing.T) { 524 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions") 525 ctx := context.TODO() 526 defer setupTest(t)() 527 528 tests := []struct { 529 name string 530 dockerfile string 531 expectedErr string 532 }{ 533 { 534 name: "empty-dockerfile", 535 dockerfile: "", 536 expectedErr: "cannot be empty", 537 }, 538 { 539 name: "empty-lines-dockerfile", 540 dockerfile: ` 541 542 543 544 `, 545 expectedErr: "file with no instructions", 546 }, 547 { 548 name: "comment-only-dockerfile", 549 dockerfile: `# this is a comment`, 550 expectedErr: "file with no instructions", 551 }, 552 } 553 554 apiclient := testEnv.APIClient() 555 556 for _, tc := range tests { 557 tc := tc 558 t.Run(tc.name, func(t *testing.T) { 559 t.Parallel() 560 561 buf := bytes.NewBuffer(nil) 562 w := tar.NewWriter(buf) 563 writeTarRecord(t, w, "Dockerfile", tc.dockerfile) 564 err := w.Close() 565 assert.NilError(t, err) 566 567 _, err = apiclient.ImageBuild(ctx, 568 buf, 569 types.ImageBuildOptions{ 570 Remove: true, 571 ForceRemove: true, 572 }) 573 574 assert.Check(t, is.Contains(err.Error(), tc.expectedErr)) 575 }) 576 } 577 } 578 579 func TestBuildPreserveOwnership(t *testing.T) { 580 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 581 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions") 582 583 ctx := context.Background() 584 585 dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name()) 586 assert.NilError(t, err) 587 588 source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) 589 defer source.Close() 590 591 apiclient := testEnv.APIClient() 592 593 for _, target := range []string{"copy_from", "copy_from_chowned"} { 594 t.Run(target, func(t *testing.T) { 595 resp, err := apiclient.ImageBuild( 596 ctx, 597 source.AsTarReader(t), 598 types.ImageBuildOptions{ 599 Remove: true, 600 ForceRemove: true, 601 Target: target, 602 }, 603 ) 604 assert.NilError(t, err) 605 606 out := bytes.NewBuffer(nil) 607 _, err = io.Copy(out, resp.Body) 608 _ = resp.Body.Close() 609 if err != nil { 610 t.Log(out) 611 } 612 assert.NilError(t, err) 613 }) 614 } 615 } 616 617 func TestBuildPlatformInvalid(t *testing.T) { 618 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions") 619 620 ctx := context.Background() 621 defer setupTest(t)() 622 623 dockerfile := `FROM busybox 624 ` 625 626 buf := bytes.NewBuffer(nil) 627 w := tar.NewWriter(buf) 628 writeTarRecord(t, w, "Dockerfile", dockerfile) 629 err := w.Close() 630 assert.NilError(t, err) 631 632 apiclient := testEnv.APIClient() 633 _, err = apiclient.ImageBuild(ctx, 634 buf, 635 types.ImageBuildOptions{ 636 Remove: true, 637 ForceRemove: true, 638 Platform: "foobar", 639 }) 640 641 assert.Assert(t, err != nil) 642 assert.ErrorContains(t, err, "unknown operating system or architecture") 643 assert.Assert(t, errdefs.IsInvalidParameter(err)) 644 } 645 646 func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) { 647 err := w.WriteHeader(&tar.Header{ 648 Name: fn, 649 Mode: 0600, 650 Size: int64(len(contents)), 651 Typeflag: '0', 652 }) 653 assert.NilError(t, err) 654 _, err = w.Write([]byte(contents)) 655 assert.NilError(t, err) 656 } 657 658 type buildLine struct { 659 Stream string 660 Aux struct { 661 ID string 662 } 663 } 664 665 func getImageIDsFromBuild(output []byte) ([]string, error) { 666 var ids []string 667 for _, line := range bytes.Split(output, []byte("\n")) { 668 if len(line) == 0 { 669 continue 670 } 671 entry := buildLine{} 672 if err := json.Unmarshal(line, &entry); err != nil { 673 return nil, err 674 } 675 if entry.Aux.ID != "" { 676 ids = append(ids, entry.Aux.ID) 677 } 678 } 679 return ids, nil 680 }