github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/integration/build/build_test.go (about) 1 package build // import "github.com/demonoid81/moby/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/demonoid81/moby/api/types" 14 "github.com/demonoid81/moby/api/types/filters" 15 "github.com/demonoid81/moby/api/types/versions" 16 "github.com/demonoid81/moby/errdefs" 17 "github.com/demonoid81/moby/pkg/jsonmessage" 18 "github.com/demonoid81/moby/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 resp, err := apiclient.ImageBuild(ctx, 211 source.AsTarReader(t), 212 types.ImageBuildOptions{ 213 Remove: true, 214 ForceRemove: true, 215 Tags: []string{"build1"}, 216 }) 217 assert.NilError(t, err) 218 _, err = io.Copy(ioutil.Discard, resp.Body) 219 resp.Body.Close() 220 assert.NilError(t, err) 221 222 image, _, err := apiclient.ImageInspectWithRaw(ctx, "build1") 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 bldName := "build-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{bldName}, 264 Labels: testLabels, 265 Target: "target-a", 266 }) 267 assert.NilError(t, err) 268 _, err = io.Copy(ioutil.Discard, resp.Body) 269 resp.Body.Close() 270 assert.NilError(t, err) 271 272 image, _, err := apiclient.ImageInspectWithRaw(ctx, bldName) 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 bldName = "build-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{bldName}, 291 Labels: testLabels, 292 Target: "target-b", 293 }) 294 assert.NilError(t, err) 295 _, err = io.Copy(ioutil.Discard, resp.Body) 296 resp.Body.Close() 297 assert.NilError(t, err) 298 299 image, _, err = apiclient.ImageInspectWithRaw(ctx, bldName) 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(ioutil.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 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 344 defer setupTest(t)() 345 // test both metadata and layer based commands as they may be implemented differently 346 dockerfile := `FROM busybox AS stage1 347 ONBUILD RUN echo 'foo' >somefile 348 ONBUILD ENV bar=baz 349 350 FROM stage1 351 # fails if ONBUILD RUN fails 352 RUN cat somefile 353 354 FROM stage1 355 RUN cat somefile` 356 357 ctx := context.Background() 358 source := fakecontext.New(t, "", 359 fakecontext.WithDockerfile(dockerfile)) 360 defer source.Close() 361 362 apiclient := testEnv.APIClient() 363 resp, err := apiclient.ImageBuild(ctx, 364 source.AsTarReader(t), 365 types.ImageBuildOptions{ 366 Remove: true, 367 ForceRemove: true, 368 }) 369 370 out := bytes.NewBuffer(nil) 371 assert.NilError(t, err) 372 _, err = io.Copy(out, resp.Body) 373 resp.Body.Close() 374 assert.NilError(t, err) 375 376 assert.Check(t, is.Contains(out.String(), "Successfully built")) 377 378 imageIDs, err := getImageIDsFromBuild(out.Bytes()) 379 assert.NilError(t, err) 380 assert.Check(t, is.Equal(3, len(imageIDs))) 381 382 image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2]) 383 assert.NilError(t, err) 384 assert.Check(t, is.Contains(image.Config.Env, "bar=baz")) 385 } 386 387 // #35403 #36122 388 func TestBuildUncleanTarFilenames(t *testing.T) { 389 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions") 390 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 391 392 ctx := context.TODO() 393 defer setupTest(t)() 394 395 dockerfile := `FROM scratch 396 COPY foo / 397 FROM scratch 398 COPY bar /` 399 400 buf := bytes.NewBuffer(nil) 401 w := tar.NewWriter(buf) 402 writeTarRecord(t, w, "Dockerfile", dockerfile) 403 writeTarRecord(t, w, "../foo", "foocontents0") 404 writeTarRecord(t, w, "/bar", "barcontents0") 405 err := w.Close() 406 assert.NilError(t, err) 407 408 apiclient := testEnv.APIClient() 409 resp, err := apiclient.ImageBuild(ctx, 410 buf, 411 types.ImageBuildOptions{ 412 Remove: true, 413 ForceRemove: true, 414 }) 415 416 out := bytes.NewBuffer(nil) 417 assert.NilError(t, err) 418 _, err = io.Copy(out, resp.Body) 419 resp.Body.Close() 420 assert.NilError(t, err) 421 422 // repeat with changed data should not cause cache hits 423 424 buf = bytes.NewBuffer(nil) 425 w = tar.NewWriter(buf) 426 writeTarRecord(t, w, "Dockerfile", dockerfile) 427 writeTarRecord(t, w, "../foo", "foocontents1") 428 writeTarRecord(t, w, "/bar", "barcontents1") 429 err = w.Close() 430 assert.NilError(t, err) 431 432 resp, err = apiclient.ImageBuild(ctx, 433 buf, 434 types.ImageBuildOptions{ 435 Remove: true, 436 ForceRemove: true, 437 }) 438 439 out = bytes.NewBuffer(nil) 440 assert.NilError(t, err) 441 _, err = io.Copy(out, resp.Body) 442 resp.Body.Close() 443 assert.NilError(t, err) 444 assert.Assert(t, !strings.Contains(out.String(), "Using cache")) 445 } 446 447 // docker/for-linux#135 448 // #35641 449 func TestBuildMultiStageLayerLeak(t *testing.T) { 450 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 451 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions") 452 ctx := context.TODO() 453 defer setupTest(t)() 454 455 // all commands need to match until COPY 456 dockerfile := `FROM busybox 457 WORKDIR /foo 458 COPY foo . 459 FROM busybox 460 WORKDIR /foo 461 COPY bar . 462 RUN [ -f bar ] 463 RUN [ ! -f foo ] 464 ` 465 466 source := fakecontext.New(t, "", 467 fakecontext.WithFile("foo", "0"), 468 fakecontext.WithFile("bar", "1"), 469 fakecontext.WithDockerfile(dockerfile)) 470 defer source.Close() 471 472 apiclient := testEnv.APIClient() 473 resp, err := apiclient.ImageBuild(ctx, 474 source.AsTarReader(t), 475 types.ImageBuildOptions{ 476 Remove: true, 477 ForceRemove: true, 478 }) 479 480 out := bytes.NewBuffer(nil) 481 assert.NilError(t, err) 482 _, err = io.Copy(out, resp.Body) 483 resp.Body.Close() 484 assert.NilError(t, err) 485 486 assert.Check(t, is.Contains(out.String(), "Successfully built")) 487 } 488 489 // #37581 490 func TestBuildWithHugeFile(t *testing.T) { 491 skip.If(t, testEnv.OSType == "windows") 492 ctx := context.TODO() 493 defer setupTest(t)() 494 495 dockerfile := `FROM busybox 496 # create a sparse file with size over 8GB 497 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 && \ 498 ls -la rnd && du -sk rnd` 499 500 buf := bytes.NewBuffer(nil) 501 w := tar.NewWriter(buf) 502 writeTarRecord(t, w, "Dockerfile", dockerfile) 503 err := w.Close() 504 assert.NilError(t, err) 505 506 apiclient := testEnv.APIClient() 507 resp, err := apiclient.ImageBuild(ctx, 508 buf, 509 types.ImageBuildOptions{ 510 Remove: true, 511 ForceRemove: true, 512 }) 513 514 out := bytes.NewBuffer(nil) 515 assert.NilError(t, err) 516 _, err = io.Copy(out, resp.Body) 517 resp.Body.Close() 518 assert.NilError(t, err) 519 assert.Check(t, is.Contains(out.String(), "Successfully built")) 520 } 521 522 func TestBuildWithEmptyDockerfile(t *testing.T) { 523 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions") 524 ctx := context.TODO() 525 defer setupTest(t)() 526 527 tests := []struct { 528 name string 529 dockerfile string 530 expectedErr string 531 }{ 532 { 533 name: "empty-dockerfile", 534 dockerfile: "", 535 expectedErr: "cannot be empty", 536 }, 537 { 538 name: "empty-lines-dockerfile", 539 dockerfile: ` 540 541 542 543 `, 544 expectedErr: "file with no instructions", 545 }, 546 { 547 name: "comment-only-dockerfile", 548 dockerfile: `# this is a comment`, 549 expectedErr: "file with no instructions", 550 }, 551 } 552 553 apiclient := testEnv.APIClient() 554 555 for _, tc := range tests { 556 tc := tc 557 t.Run(tc.name, func(t *testing.T) { 558 t.Parallel() 559 560 buf := bytes.NewBuffer(nil) 561 w := tar.NewWriter(buf) 562 writeTarRecord(t, w, "Dockerfile", tc.dockerfile) 563 err := w.Close() 564 assert.NilError(t, err) 565 566 _, err = apiclient.ImageBuild(ctx, 567 buf, 568 types.ImageBuildOptions{ 569 Remove: true, 570 ForceRemove: true, 571 }) 572 573 assert.Check(t, is.Contains(err.Error(), tc.expectedErr)) 574 }) 575 } 576 } 577 578 func TestBuildPreserveOwnership(t *testing.T) { 579 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 580 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions") 581 582 ctx := context.Background() 583 584 dockerfile, err := ioutil.ReadFile("testdata/Dockerfile.testBuildPreserveOwnership") 585 assert.NilError(t, err) 586 587 source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) 588 defer source.Close() 589 590 apiclient := testEnv.APIClient() 591 592 for _, target := range []string{"copy_from", "copy_from_chowned"} { 593 t.Run(target, func(t *testing.T) { 594 resp, err := apiclient.ImageBuild( 595 ctx, 596 source.AsTarReader(t), 597 types.ImageBuildOptions{ 598 Remove: true, 599 ForceRemove: true, 600 Target: target, 601 }, 602 ) 603 assert.NilError(t, err) 604 605 out := bytes.NewBuffer(nil) 606 _, err = io.Copy(out, resp.Body) 607 _ = resp.Body.Close() 608 if err != nil { 609 t.Log(out) 610 } 611 assert.NilError(t, err) 612 }) 613 } 614 } 615 616 func TestBuildPlatformInvalid(t *testing.T) { 617 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions") 618 619 ctx := context.Background() 620 defer setupTest(t)() 621 622 dockerfile := `FROM busybox 623 ` 624 625 buf := bytes.NewBuffer(nil) 626 w := tar.NewWriter(buf) 627 writeTarRecord(t, w, "Dockerfile", dockerfile) 628 err := w.Close() 629 assert.NilError(t, err) 630 631 apiclient := testEnv.APIClient() 632 _, err = apiclient.ImageBuild(ctx, 633 buf, 634 types.ImageBuildOptions{ 635 Remove: true, 636 ForceRemove: true, 637 Platform: "foobar", 638 }) 639 640 assert.Assert(t, err != nil) 641 assert.ErrorContains(t, err, "unknown operating system or architecture") 642 assert.Assert(t, errdefs.IsInvalidParameter(err)) 643 } 644 645 func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) { 646 err := w.WriteHeader(&tar.Header{ 647 Name: fn, 648 Mode: 0600, 649 Size: int64(len(contents)), 650 Typeflag: '0', 651 }) 652 assert.NilError(t, err) 653 _, err = w.Write([]byte(contents)) 654 assert.NilError(t, err) 655 } 656 657 type buildLine struct { 658 Stream string 659 Aux struct { 660 ID string 661 } 662 } 663 664 func getImageIDsFromBuild(output []byte) ([]string, error) { 665 var ids []string 666 for _, line := range bytes.Split(output, []byte("\n")) { 667 if len(line) == 0 { 668 continue 669 } 670 entry := buildLine{} 671 if err := json.Unmarshal(line, &entry); err != nil { 672 return nil, err 673 } 674 if entry.Aux.ID != "" { 675 ids = append(ids, entry.Aux.ID) 676 } 677 } 678 return ids, nil 679 }