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