diff --git a/builder/dockerfile/copy.go b/builder/dockerfile/copy.go index 4e1c7e06dc..2c08b69367 100644 --- a/builder/dockerfile/copy.go +++ b/builder/dockerfile/copy.go @@ -556,13 +556,15 @@ func copyFile(archiver Archiver, source, dest *copyEndpoint, identity *idtools.I return errors.Wrapf(err, "failed to create new directory") } } else { + // Normal containers if identity == nil { - if err := os.MkdirAll(filepath.Dir(dest.path), 0755); err != nil { + // Use system.MkdirAll here, which is a custom version of os.MkdirAll + // modified for use on Windows to handle volume GUID paths (\\?\{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\path\) + if err := system.MkdirAll(filepath.Dir(dest.path), 0755, ""); err != nil { return err } } else { if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest.path), 0755, *identity); err != nil { - // Normal containers return errors.Wrapf(err, "failed to create new directory") } } diff --git a/integration/build/build_test.go b/integration/build/build_test.go index 183e714226..b48921d1a4 100644 --- a/integration/build/build_test.go +++ b/integration/build/build_test.go @@ -135,6 +135,58 @@ func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) { } } +// 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 := context.Background() + + dockerfile, err := ioutil.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) + assert.NilError(t, err) + _, 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) { skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.35"), "broken in earlier versions") skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") diff --git a/integration/build/testdata/Dockerfile.TestBuildMultiStageCopy b/integration/build/testdata/Dockerfile.TestBuildMultiStageCopy new file mode 100644 index 0000000000..52b0acff16 --- /dev/null +++ b/integration/build/testdata/Dockerfile.TestBuildMultiStageCopy @@ -0,0 +1,20 @@ +FROM busybox AS base +RUN mkdir existingdir + +FROM base AS source +RUN echo "Hello World" > /hello + +FROM base AS copy_to_root +COPY --from=source /hello /hello + +FROM base AS copy_to_newdir +COPY --from=source /hello /newdir/hello + +FROM base AS copy_to_newdir_nested +COPY --from=source /hello /newdir/newsubdir/hello + +FROM base AS copy_to_existingdir +COPY --from=source /hello /existingdir/hello + +FROM base AS copy_to_newsubdir +COPY --from=source /hello /existingdir/newsubdir/hello