| package build // import "github.com/docker/docker/integration/build" | |
| import ( | |
| "archive/tar" | |
| "bytes" | |
| "context" | |
| "encoding/json" | |
| "io" | |
| "os" | |
| "strings" | |
| "testing" | |
| "time" | |
| "github.com/docker/docker/api/types" | |
| "github.com/docker/docker/api/types/container" | |
| "github.com/docker/docker/api/types/events" | |
| "github.com/docker/docker/api/types/filters" | |
| "github.com/docker/docker/errdefs" | |
| "github.com/docker/docker/pkg/jsonmessage" | |
| "github.com/docker/docker/testutil" | |
| "github.com/docker/docker/testutil/fakecontext" | |
| "gotest.tools/v3/assert" | |
| is "gotest.tools/v3/assert/cmp" | |
| "gotest.tools/v3/skip" | |
| ) | |
| func TestBuildWithRemoveAndForceRemove(t *testing.T) { | |
| ctx := setupTest(t) | |
| cases := []struct { | |
| name string | |
| dockerfile string | |
| numberOfIntermediateContainers int | |
| rm bool | |
| forceRm bool | |
| }{ | |
| { | |
| name: "successful build with no removal", | |
| dockerfile: `FROM busybox | |
| RUN exit 0 | |
| RUN exit 0`, | |
| numberOfIntermediateContainers: 2, | |
| rm: false, | |
| forceRm: false, | |
| }, | |
| { | |
| name: "successful build with remove", | |
| dockerfile: `FROM busybox | |
| RUN exit 0 | |
| RUN exit 0`, | |
| numberOfIntermediateContainers: 0, | |
| rm: true, | |
| forceRm: false, | |
| }, | |
| { | |
| name: "successful build with remove and force remove", | |
| dockerfile: `FROM busybox | |
| RUN exit 0 | |
| RUN exit 0`, | |
| numberOfIntermediateContainers: 0, | |
| rm: true, | |
| forceRm: true, | |
| }, | |
| { | |
| name: "failed build with no removal", | |
| dockerfile: `FROM busybox | |
| RUN exit 0 | |
| RUN exit 1`, | |
| numberOfIntermediateContainers: 2, | |
| rm: false, | |
| forceRm: false, | |
| }, | |
| { | |
| name: "failed build with remove", | |
| dockerfile: `FROM busybox | |
| RUN exit 0 | |
| RUN exit 1`, | |
| numberOfIntermediateContainers: 1, | |
| rm: true, | |
| forceRm: false, | |
| }, | |
| { | |
| name: "failed build with remove and force remove", | |
| dockerfile: `FROM busybox | |
| RUN exit 0 | |
| RUN exit 1`, | |
| numberOfIntermediateContainers: 0, | |
| rm: true, | |
| forceRm: true, | |
| }, | |
| } | |
| client := testEnv.APIClient() | |
| for _, c := range cases { | |
| c := c | |
| t.Run(c.name, func(t *testing.T) { | |
| t.Parallel() | |
| ctx := testutil.StartSpan(ctx, t) | |
| dockerfile := []byte(c.dockerfile) | |
| buff := bytes.NewBuffer(nil) | |
| tw := tar.NewWriter(buff) | |
| assert.NilError(t, tw.WriteHeader(&tar.Header{ | |
| Name: "Dockerfile", | |
| Size: int64(len(dockerfile)), | |
| })) | |
| _, err := tw.Write(dockerfile) | |
| assert.NilError(t, err) | |
| assert.NilError(t, tw.Close()) | |
| resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true}) | |
| assert.NilError(t, err) | |
| defer resp.Body.Close() | |
| filter, err := buildContainerIdsFilter(resp.Body) | |
| assert.NilError(t, err) | |
| remainingContainers, err := client.ContainerList(ctx, container.ListOptions{Filters: filter, All: true}) | |
| assert.NilError(t, err) | |
| assert.Equal(t, c.numberOfIntermediateContainers, len(remainingContainers), "Expected %v remaining intermediate containers, got %v", c.numberOfIntermediateContainers, len(remainingContainers)) | |
| }) | |
| } | |
| } | |
| func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) { | |
| const intermediateContainerPrefix = " ---> Running in " | |
| filter := filters.NewArgs() | |
| dec := json.NewDecoder(buildOutput) | |
| for { | |
| m := jsonmessage.JSONMessage{} | |
| err := dec.Decode(&m) | |
| if err == io.EOF { | |
| return filter, nil | |
| } | |
| if err != nil { | |
| return filter, err | |
| } | |
| if ix := strings.Index(m.Stream, intermediateContainerPrefix); ix != -1 { | |
| filter.Add("id", strings.TrimSpace(m.Stream[ix+len(intermediateContainerPrefix):])) | |
| } | |
| } | |
| } | |
| // TestBuildMultiStageCopy verifies that copying between stages works correctly. | |
| // | |
| // Regression test for docker/for-win#4349, ENGCORE-935, where creating the target | |
| // directory failed on Windows, because `os.MkdirAll()` was called with a volume | |
| // GUID path (\\?\Volume{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\newdir\hello}), | |
| // which currently isn't supported by Golang. | |
| func TestBuildMultiStageCopy(t *testing.T) { | |
| ctx := setupTest(t) | |
| dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name()) | |
| assert.NilError(t, err) | |
| source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) | |
| defer source.Close() | |
| apiclient := testEnv.APIClient() | |
| for _, target := range []string{"copy_to_root", "copy_to_newdir", "copy_to_newdir_nested", "copy_to_existingdir", "copy_to_newsubdir"} { | |
| t.Run(target, func(t *testing.T) { | |
| imgName := strings.ToLower(t.Name()) | |
| resp, err := apiclient.ImageBuild( | |
| ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| Target: target, | |
| Tags: []string{imgName}, | |
| }, | |
| ) | |
| assert.NilError(t, err) | |
| out := bytes.NewBuffer(nil) | |
| _, err = io.Copy(out, resp.Body) | |
| _ = resp.Body.Close() | |
| if err != nil { | |
| t.Log(out) | |
| } | |
| assert.NilError(t, err) | |
| // verify the image was successfully built | |
| _, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) | |
| if err != nil { | |
| t.Log(out) | |
| } | |
| assert.NilError(t, err) | |
| }) | |
| } | |
| } | |
| func TestBuildMultiStageParentConfig(t *testing.T) { | |
| dockerfile := ` | |
| FROM busybox AS stage0 | |
| ENV WHO=parent | |
| WORKDIR /foo | |
| FROM stage0 | |
| ENV WHO=sibling1 | |
| WORKDIR sub1 | |
| FROM stage0 | |
| WORKDIR sub2 | |
| ` | |
| ctx := setupTest(t) | |
| source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) | |
| defer source.Close() | |
| apiclient := testEnv.APIClient() | |
| imgName := strings.ToLower(t.Name()) | |
| resp, err := apiclient.ImageBuild(ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| Tags: []string{imgName}, | |
| }) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(io.Discard, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| img, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) | |
| assert.NilError(t, err) | |
| expected := "/foo/sub2" | |
| if testEnv.DaemonInfo.OSType == "windows" { | |
| expected = `C:\foo\sub2` | |
| } | |
| assert.Check(t, is.Equal(expected, img.Config.WorkingDir)) | |
| assert.Check(t, is.Contains(img.Config.Env, "WHO=parent")) | |
| } | |
| // Test cases in #36996 | |
| func TestBuildLabelWithTargets(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") | |
| imgName := strings.ToLower(t.Name() + "-a") | |
| testLabels := map[string]string{ | |
| "foo": "bar", | |
| "dead": "beef", | |
| } | |
| dockerfile := ` | |
| FROM busybox AS target-a | |
| CMD ["/dev"] | |
| LABEL label-a=inline-a | |
| FROM busybox AS target-b | |
| CMD ["/dist"] | |
| LABEL label-b=inline-b | |
| ` | |
| ctx := setupTest(t) | |
| source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) | |
| defer source.Close() | |
| apiclient := testEnv.APIClient() | |
| // For `target-a` build | |
| resp, err := apiclient.ImageBuild(ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| Tags: []string{imgName}, | |
| Labels: testLabels, | |
| Target: "target-a", | |
| }) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(io.Discard, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| img, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) | |
| assert.NilError(t, err) | |
| testLabels["label-a"] = "inline-a" | |
| for k, v := range testLabels { | |
| x, ok := img.Config.Labels[k] | |
| assert.Assert(t, ok) | |
| assert.Assert(t, x == v) | |
| } | |
| // For `target-b` build | |
| imgName = strings.ToLower(t.Name() + "-b") | |
| delete(testLabels, "label-a") | |
| resp, err = apiclient.ImageBuild(ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| Tags: []string{imgName}, | |
| Labels: testLabels, | |
| Target: "target-b", | |
| }) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(io.Discard, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| img, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) | |
| assert.NilError(t, err) | |
| testLabels["label-b"] = "inline-b" | |
| for k, v := range testLabels { | |
| x, ok := img.Config.Labels[k] | |
| assert.Check(t, ok) | |
| assert.Check(t, x == v) | |
| } | |
| } | |
| func TestBuildWithEmptyLayers(t *testing.T) { | |
| const dockerfile = ` | |
| FROM busybox | |
| COPY 1/ /target/ | |
| COPY 2/ /target/ | |
| COPY 3/ /target/ | |
| ` | |
| ctx := setupTest(t) | |
| source := fakecontext.New(t, "", | |
| fakecontext.WithDockerfile(dockerfile), | |
| fakecontext.WithFile("1/a", "asdf"), | |
| fakecontext.WithFile("2/a", "asdf"), | |
| fakecontext.WithFile("3/a", "asdf")) | |
| defer source.Close() | |
| apiclient := testEnv.APIClient() | |
| resp, err := apiclient.ImageBuild(ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(io.Discard, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| } | |
| // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to | |
| // multiple subsequent stages | |
| // #35652 | |
| func TestBuildMultiStageOnBuild(t *testing.T) { | |
| ctx := setupTest(t) | |
| // test both metadata and layer based commands as they may be implemented differently | |
| const dockerfile = ` | |
| FROM busybox AS stage1 | |
| ONBUILD RUN echo 'foo' >somefile | |
| ONBUILD ENV bar=baz | |
| FROM stage1 | |
| # fails if ONBUILD RUN fails | |
| RUN cat somefile | |
| FROM stage1 | |
| RUN cat somefile` | |
| source := fakecontext.New(t, "", | |
| fakecontext.WithDockerfile(dockerfile)) | |
| defer source.Close() | |
| apiclient := testEnv.APIClient() | |
| resp, err := apiclient.ImageBuild(ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| out := bytes.NewBuffer(nil) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Contains(out.String(), "Successfully built")) | |
| imageIDs, err := getImageIDsFromBuild(out.Bytes()) | |
| assert.NilError(t, err) | |
| assert.Assert(t, is.Equal(3, len(imageIDs))) | |
| img, _, err := apiclient.ImageInspectWithRaw(ctx, imageIDs[2]) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Contains(img.Config.Env, "bar=baz")) | |
| } | |
| // #35403 #36122 | |
| func TestBuildUncleanTarFilenames(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") | |
| ctx := setupTest(t) | |
| const dockerfile = ` | |
| FROM scratch | |
| COPY foo / | |
| FROM scratch | |
| COPY bar / | |
| ` | |
| buf := bytes.NewBuffer(nil) | |
| w := tar.NewWriter(buf) | |
| writeTarRecord(t, w, "Dockerfile", dockerfile) | |
| writeTarRecord(t, w, "../foo", "foocontents0") | |
| writeTarRecord(t, w, "/bar", "barcontents0") | |
| err := w.Close() | |
| assert.NilError(t, err) | |
| apiclient := testEnv.APIClient() | |
| resp, err := apiclient.ImageBuild(ctx, | |
| buf, | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| out := bytes.NewBuffer(nil) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| // repeat with changed data should not cause cache hits | |
| buf = bytes.NewBuffer(nil) | |
| w = tar.NewWriter(buf) | |
| writeTarRecord(t, w, "Dockerfile", dockerfile) | |
| writeTarRecord(t, w, "../foo", "foocontents1") | |
| writeTarRecord(t, w, "/bar", "barcontents1") | |
| err = w.Close() | |
| assert.NilError(t, err) | |
| resp, err = apiclient.ImageBuild(ctx, | |
| buf, | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| out = bytes.NewBuffer(nil) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| assert.Assert(t, !strings.Contains(out.String(), "Using cache")) | |
| } | |
| // docker/for-linux#135 | |
| // #35641 | |
| func TestBuildMultiStageLayerLeak(t *testing.T) { | |
| ctx := setupTest(t) | |
| // all commands need to match until COPY | |
| const dockerfile = ` | |
| FROM busybox | |
| WORKDIR /foo | |
| COPY foo . | |
| FROM busybox | |
| WORKDIR /foo | |
| COPY bar . | |
| RUN [ -f bar ] | |
| RUN [ ! -f foo ] | |
| ` | |
| source := fakecontext.New(t, "", | |
| fakecontext.WithFile("foo", "0"), | |
| fakecontext.WithFile("bar", "1"), | |
| fakecontext.WithDockerfile(dockerfile)) | |
| defer source.Close() | |
| apiClient := testEnv.APIClient() | |
| resp, err := apiClient.ImageBuild(ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| out := bytes.NewBuffer(nil) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Contains(out.String(), "Successfully built")) | |
| } | |
| // #37581 | |
| // #40444 (Windows Containers only) | |
| func TestBuildWithHugeFile(t *testing.T) { | |
| ctx := setupTest(t) | |
| var dockerfile string | |
| if testEnv.DaemonInfo.OSType == "windows" { | |
| dockerfile = ` | |
| FROM busybox | |
| # create a file with size of 8GB | |
| RUN powershell "fsutil.exe file createnew bigfile.txt 8589934592 ; dir bigfile.txt" | |
| ` | |
| } else { | |
| dockerfile = ` | |
| FROM busybox | |
| # create a sparse file with size over 8GB | |
| 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 \ | |
| && ls -la rnd && du -sk rnd | |
| ` | |
| } | |
| buf := bytes.NewBuffer(nil) | |
| w := tar.NewWriter(buf) | |
| writeTarRecord(t, w, "Dockerfile", dockerfile) | |
| err := w.Close() | |
| assert.NilError(t, err) | |
| apiClient := testEnv.APIClient() | |
| resp, err := apiClient.ImageBuild(ctx, | |
| buf, | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| out := bytes.NewBuffer(nil) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Contains(out.String(), "Successfully built")) | |
| } | |
| func TestBuildWCOWSandboxSize(t *testing.T) { | |
| t.Skip("FLAKY_TEST that needs to be fixed; see https://github.com/moby/moby/issues/42743") | |
| skip.If(t, testEnv.DaemonInfo.OSType != "windows", "only Windows has sandbox size control") | |
| ctx := setupTest(t) | |
| const dockerfile = ` | |
| FROM busybox AS intermediate | |
| WORKDIR C:\\stuff | |
| # Create and delete a 21GB file | |
| RUN fsutil file createnew C:\\stuff\\bigfile_0.txt 22548578304 && del bigfile_0.txt | |
| # Create three 7GB files | |
| RUN fsutil file createnew C:\\stuff\\bigfile_1.txt 7516192768 | |
| RUN fsutil file createnew C:\\stuff\\bigfile_2.txt 7516192768 | |
| RUN fsutil file createnew C:\\stuff\\bigfile_3.txt 7516192768 | |
| # Copy that 21GB of data out into a new target | |
| FROM busybox | |
| COPY --from=intermediate C:\\stuff C:\\stuff | |
| ` | |
| buf := bytes.NewBuffer(nil) | |
| w := tar.NewWriter(buf) | |
| writeTarRecord(t, w, "Dockerfile", dockerfile) | |
| err := w.Close() | |
| assert.NilError(t, err) | |
| apiClient := testEnv.APIClient() | |
| resp, err := apiClient.ImageBuild(ctx, | |
| buf, | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| out := bytes.NewBuffer(nil) | |
| assert.NilError(t, err) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.Check(t, resp.Body.Close()) | |
| assert.NilError(t, err) | |
| // The test passes if either: | |
| // - the image build succeeded; or | |
| // - The "COPY --from=intermediate" step ran out of space during re-exec'd writing of the transport layer information to hcsshim's temp directory | |
| // The latter case means we finished the COPY operation, so the sandbox must have been larger than 20GB, which was the test, | |
| // and _then_ ran out of space on the host during `importLayer` in the WindowsFilter graph driver, while committing the layer. | |
| // See https://github.com/moby/moby/pull/41636#issuecomment-723038517 for more details on the operations being done here. | |
| // Specifically, this happens on the Docker Jenkins CI Windows-RS5 build nodes. | |
| // The two parts of the acceptable-failure case are on different lines, so we need two regexp checks. | |
| assert.Check(t, is.Regexp("Successfully built|COPY --from=intermediate", out.String())) | |
| assert.Check(t, is.Regexp("Successfully built|re-exec error: exit status 1: output: write.*daemon\\\\\\\\tmp\\\\\\\\hcs.*bigfile_[1-3].txt: There is not enough space on the disk.", out.String())) | |
| } | |
| func TestBuildWithEmptyDockerfile(t *testing.T) { | |
| ctx := setupTest(t) | |
| tests := []struct { | |
| name string | |
| dockerfile string | |
| expectedErr string | |
| }{ | |
| { | |
| name: "empty-dockerfile", | |
| dockerfile: "", | |
| expectedErr: "cannot be empty", | |
| }, | |
| { | |
| name: "empty-lines-dockerfile", | |
| dockerfile: ` | |
| `, | |
| expectedErr: "file with no instructions", | |
| }, | |
| { | |
| name: "comment-only-dockerfile", | |
| dockerfile: `# this is a comment`, | |
| expectedErr: "file with no instructions", | |
| }, | |
| } | |
| apiClient := testEnv.APIClient() | |
| for _, tc := range tests { | |
| tc := tc | |
| t.Run(tc.name, func(t *testing.T) { | |
| t.Parallel() | |
| buf := bytes.NewBuffer(nil) | |
| w := tar.NewWriter(buf) | |
| writeTarRecord(t, w, "Dockerfile", tc.dockerfile) | |
| err := w.Close() | |
| assert.NilError(t, err) | |
| _, err = apiClient.ImageBuild(ctx, | |
| buf, | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| }) | |
| assert.Check(t, is.Contains(err.Error(), tc.expectedErr)) | |
| }) | |
| } | |
| } | |
| func TestBuildPreserveOwnership(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") | |
| ctx := setupTest(t) | |
| dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name()) | |
| assert.NilError(t, err) | |
| source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) | |
| defer source.Close() | |
| apiClient := testEnv.APIClient() | |
| for _, target := range []string{"copy_from", "copy_from_chowned"} { | |
| t.Run(target, func(t *testing.T) { | |
| ctx := testutil.StartSpan(ctx, t) | |
| resp, err := apiClient.ImageBuild( | |
| ctx, | |
| source.AsTarReader(t), | |
| types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| Target: target, | |
| }, | |
| ) | |
| assert.NilError(t, err) | |
| out := bytes.NewBuffer(nil) | |
| _, err = io.Copy(out, resp.Body) | |
| _ = resp.Body.Close() | |
| if err != nil { | |
| t.Log(out) | |
| } | |
| assert.NilError(t, err) | |
| }) | |
| } | |
| } | |
| func TestBuildPlatformInvalid(t *testing.T) { | |
| ctx := setupTest(t) | |
| buf := bytes.NewBuffer(nil) | |
| w := tar.NewWriter(buf) | |
| writeTarRecord(t, w, "Dockerfile", `FROM busybox`) | |
| err := w.Close() | |
| assert.NilError(t, err) | |
| _, err = testEnv.APIClient().ImageBuild(ctx, buf, types.ImageBuildOptions{ | |
| Remove: true, | |
| ForceRemove: true, | |
| Platform: "foobar", | |
| }) | |
| assert.Check(t, is.ErrorContains(err, "unknown operating system or architecture")) | |
| assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter)) | |
| } | |
| // TestBuildWorkdirNoCacheMiss is a regression test for https://github.com/moby/moby/issues/47627 | |
| func TestBuildWorkdirNoCacheMiss(t *testing.T) { | |
| ctx := setupTest(t) | |
| for _, tc := range []struct { | |
| name string | |
| dockerfile string | |
| }{ | |
| {name: "trailing slash", dockerfile: "FROM busybox\nWORKDIR /foo/"}, | |
| {name: "no trailing slash", dockerfile: "FROM busybox\nWORKDIR /foo"}, | |
| } { | |
| dockerfile := tc.dockerfile | |
| t.Run(tc.name, func(t *testing.T) { | |
| source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) | |
| defer source.Close() | |
| apiClient := testEnv.APIClient() | |
| buildAndGetID := func() string { | |
| resp, err := apiClient.ImageBuild(ctx, source.AsTarReader(t), types.ImageBuildOptions{ | |
| Version: types.BuilderV1, | |
| }) | |
| assert.NilError(t, err) | |
| defer resp.Body.Close() | |
| id := readBuildImageIDs(t, resp.Body) | |
| assert.Check(t, id != "") | |
| return id | |
| } | |
| firstId := buildAndGetID() | |
| secondId := buildAndGetID() | |
| assert.Check(t, is.Equal(firstId, secondId), "expected cache to be used") | |
| }) | |
| } | |
| } | |
| func TestBuildEmitsImageCreateEvent(t *testing.T) { | |
| ctx := setupTest(t) | |
| dockerfile := "FROM busybox\nRUN echo hello > /hello" | |
| source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) | |
| defer source.Close() | |
| apiClient := testEnv.APIClient() | |
| for _, builderVersion := range []types.BuilderVersion{types.BuilderV1, types.BuilderBuildKit} { | |
| builderVersion := builderVersion | |
| t.Run("v"+string(builderVersion), func(t *testing.T) { | |
| if builderVersion == types.BuilderBuildKit { | |
| skip.If(t, testEnv.UsingSnapshotter(), | |
| "FIXME: Passing a context via a tarball is not supported with the containerd image store. See: https://github.com/moby/moby/issues/47717") | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Buildkit is not supported on Windows") | |
| } | |
| ctx, cancel := context.WithCancel(ctx) | |
| defer cancel() | |
| since := time.Now() | |
| resp, err := apiClient.ImageBuild(ctx, source.AsTarReader(t), types.ImageBuildOptions{ | |
| Version: builderVersion, | |
| NoCache: true, | |
| }) | |
| assert.NilError(t, err) | |
| defer resp.Body.Close() | |
| out := bytes.NewBuffer(nil) | |
| _, err = io.Copy(out, resp.Body) | |
| assert.NilError(t, err) | |
| t.Log(out.String()) | |
| eventsChan, errs := apiClient.Events(ctx, events.ListOptions{ | |
| Since: since.Format(time.RFC3339Nano), | |
| Until: time.Now().Format(time.RFC3339Nano), | |
| }) | |
| imageCreateEvts := 0 | |
| finished := false | |
| for !finished { | |
| select { | |
| case evt := <-eventsChan: | |
| t.Log("Got event type:", evt.Type, "action:", evt.Action) | |
| if evt.Type == events.ImageEventType && evt.Action == events.ActionCreate { | |
| imageCreateEvts++ | |
| } | |
| case err := <-errs: | |
| assert.Check(t, err == nil || err == io.EOF) | |
| finished = true | |
| } | |
| } | |
| assert.Check(t, is.Equal(1, imageCreateEvts)) | |
| }) | |
| } | |
| } | |
| func readBuildImageIDs(t *testing.T, rd io.Reader) string { | |
| t.Helper() | |
| decoder := json.NewDecoder(rd) | |
| for { | |
| var jm jsonmessage.JSONMessage | |
| if err := decoder.Decode(&jm); err != nil { | |
| if err == io.EOF { | |
| break | |
| } | |
| assert.NilError(t, err) | |
| } | |
| if jm.Aux == nil { | |
| continue | |
| } | |
| var auxId struct { | |
| ID string `json:"ID"` | |
| } | |
| if json.Unmarshal(*jm.Aux, &auxId); auxId.ID != "" { | |
| return auxId.ID | |
| } | |
| } | |
| return "" | |
| } | |
| func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) { | |
| err := w.WriteHeader(&tar.Header{ | |
| Name: fn, | |
| Mode: 0o600, | |
| Size: int64(len(contents)), | |
| Typeflag: '0', | |
| }) | |
| assert.NilError(t, err) | |
| _, err = w.Write([]byte(contents)) | |
| assert.NilError(t, err) | |
| } | |
| type buildLine struct { | |
| Stream string | |
| Aux struct { | |
| ID string | |
| } | |
| } | |
| func getImageIDsFromBuild(output []byte) ([]string, error) { | |
| var ids []string | |
| for _, line := range bytes.Split(output, []byte("\n")) { | |
| if len(line) == 0 { | |
| continue | |
| } | |
| entry := buildLine{} | |
| if err := json.Unmarshal(line, &entry); err != nil { | |
| return nil, err | |
| } | |
| if entry.Aux.ID != "" { | |
| ids = append(ids, entry.Aux.ID) | |
| } | |
| } | |
| return ids, nil | |
| } |