diff --git a/client/client_test.go b/client/client_test.go index c846db9231af..5b5fa7e1e6e7 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1561,15 +1561,22 @@ func testLocalSymlinkEscape(t *testing.T, sb integration.Sandbox) { } func testRelativeWorkDir(t *testing.T, sb integration.Sandbox) { - requiresLinux(t) c, err := New(sb.Context(), sb.Address()) require.NoError(t, err) defer c.Close() - pwd := llb.Image("docker.io/library/busybox:latest"). + imgName := integration.UnixOrWindows( + "docker.io/library/busybox:latest", + "mcr.microsoft.com/windows/nanoserver:ltsc2022", + ) + cmdStr := integration.UnixOrWindows( + `sh -c "pwd > /out/pwd"`, + `cmd /C "cd > /out/pwd"`, + ) + pwd := llb.Image(imgName). Dir("test1"). Dir("test2"). - Run(llb.Shlex(`sh -c "pwd > /out/pwd"`)). + Run(llb.Shlex(cmdStr)). AddMount("/out", llb.Scratch()) def, err := pwd.Marshal(sb.Context()) @@ -1589,7 +1596,11 @@ func testRelativeWorkDir(t *testing.T, sb integration.Sandbox) { dt, err := os.ReadFile(filepath.Join(destDir, "pwd")) require.NoError(t, err) - require.Equal(t, []byte("/test1/test2\n"), dt) + pathStr := integration.UnixOrWindows( + "/test1/test2\n", + "C:\\test1\\test2\r\n", + ) + require.Equal(t, []byte(pathStr), dt) } // TODO: remove this test once `client.SolveOpt.LocalDirs`, now marked as deprecated, is removed. @@ -7480,7 +7491,7 @@ func testMergeOp(t *testing.T, sb integration.Sandbox) { File(llb.Mkfile("bar/D", 0644, []byte("D"))). File(llb.Mkfile("bar/E", 0755, nil)). File(llb.Mkfile("qaz", 0644, nil)), - // /foo from stateE is not here because it is deleted in stateB, which is part of a submerge of mergeD + // /foo from stateE is not here because it is deleted in stateB, which is part of a submerge of mergeD ) } diff --git a/executor/oci/spec.go b/executor/oci/spec.go index fe0b03c4a1c1..3ec903583508 100644 --- a/executor/oci/spec.go +++ b/executor/oci/spec.go @@ -2,6 +2,7 @@ package oci import ( "context" + "github.com/moby/buildkit/util/system" "os" "path" "path/filepath" @@ -201,8 +202,8 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou return nil, nil, err } s.Mounts = append(s.Mounts, specs.Mount{ - Destination: m.Dest, - Type: mount.Type, + Destination: system.GetAbsolutePath(m.Dest), + Type: getMountType(mount.Type), Source: mount.Source, Options: mount.Options, }) @@ -243,6 +244,10 @@ type submounts struct { func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error) { if path.Join("/", subPath) == "/" { + // for Windows, the mounting by HCS doesn't go through + // WCIFS, hence we have to give the direct path of the + // mount, which is in the /Files subdirectory. + m.Source = getCompleteSourcePath(m.Source) return m, nil } if s.m == nil { diff --git a/executor/oci/spec_unix.go b/executor/oci/spec_unix.go new file mode 100644 index 000000000000..34e549f9d21b --- /dev/null +++ b/executor/oci/spec_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package oci + +// no effect for non-Windows +func getMountType(mType string) string { + return mType +} + +// no effect for non-Windows +func getCompleteSourcePath(p string) string { + return p +} diff --git a/executor/oci/spec_windows.go b/executor/oci/spec_windows.go index d3059c3034f4..bc59dfc3c81b 100644 --- a/executor/oci/spec_windows.go +++ b/executor/oci/spec_windows.go @@ -110,3 +110,16 @@ func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) { m.Source = src return m, func() error { return nil }, nil } + +func getMountType(_ string) string { + // HCS shim doesn't expect a named type + // for the mount. + return "" +} + +// For Windows, the mounting by HCS doesn't go through +// WCIFS, hence we have to give the direct path of the +// mount, which is in the /Files subdirectory. +func getCompleteSourcePath(p string) string { + return filepath.Join(p, "Files") +} diff --git a/frontend/dockerfile/dockerfile2llb/convert_runmount.go b/frontend/dockerfile/dockerfile2llb/convert_runmount.go index 1044ec360014..87be03c7e6b4 100644 --- a/frontend/dockerfile/dockerfile2llb/convert_runmount.go +++ b/frontend/dockerfile/dockerfile2llb/convert_runmount.go @@ -2,6 +2,7 @@ package dockerfile2llb import ( "context" + "github.com/moby/buildkit/util/system" "os" "path" "path/filepath" @@ -118,7 +119,7 @@ func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []* mountOpts = append(mountOpts, llb.AsPersistentCacheDir(opt.cacheIDNamespace+"/"+mount.CacheID, sharing)) } target := mount.Target - if !filepath.IsAbs(filepath.Clean(mount.Target)) { + if !system.IsAbsolutePath(filepath.Clean(mount.Target)) { dir, err := d.state.GetDir(context.TODO()) if err != nil { return nil, err diff --git a/frontend/dockerfile/dockerfile_mount_test.go b/frontend/dockerfile/dockerfile_mount_test.go index 0423145eabd5..27cac73c9bbf 100644 --- a/frontend/dockerfile/dockerfile_mount_test.go +++ b/frontend/dockerfile/dockerfile_mount_test.go @@ -36,13 +36,21 @@ func init() { } func testMountContext(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + + ` FROM busybox RUN --mount=target=/context [ "$(cat /context/testfile)" == "contents0" ] -`) +`, + ` +FROM nanoserver +USER ContainerAdministrator +RUN --mount=target=/context cmd /V:on /C "set /p tfcontent= out/foo @@ -174,7 +199,20 @@ RUN --mount=from=build,src=out,target=/out,rw touch /out/bar && cat /dev/urandom from scratch COPY --from=second /unique /unique -`) +`, + ` +FROM nanoserver AS build +COPY cachebust / +RUN mkdir out && echo foo> out\foo + +FROM nanoserver AS second +USER ContainerAdministrator +RUN --mount=from=build,src=out,target=/out,rw echo %RANDOM% > \unique + +FROM nanoserver +COPY --from=second /unique /unique +`, + )) dir := integration.Tmpdir( t, @@ -261,15 +299,23 @@ RUN --mount=type=cache,target=/mycache,uid=1001,gid=1002,mode=0751 [ "$(stat -c } func testCacheMountDefaultID(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox RUN --mount=type=cache,target=/mycache touch /mycache/foo RUN --mount=type=cache,target=/mycache2 [ ! -f /mycache2/foo ] RUN --mount=type=cache,target=/mycache [ -f /mycache/foo ] -`) +`, + ` +FROM nanoserver +USER ContainerAdministrator +RUN --mount=type=cache,target=/mycache echo hello > \mycache\foo +RUN --mount=type=cache,target=/mycache2 IF NOT EXIST C:\mycache2\foo (exit 0) ELSE (exit 1) +RUN --mount=type=cache,target=/mycache dir \mycache\foo +`, + )) dir := integration.Tmpdir( t, @@ -290,15 +336,23 @@ RUN --mount=type=cache,target=/mycache [ -f /mycache/foo ] } func testMountEnvVar(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox ENV SOME_PATH=/mycache RUN --mount=type=cache,target=/mycache touch /mycache/foo RUN --mount=type=cache,target=$SOME_PATH [ -f $SOME_PATH/foo ] -`) +`, + ` +FROM nanoserver +USER ContainerAdministrator +ENV SOME_PATH=mycache +RUN --mount=type=cache,target=/mycache echo hello > \mycache\foo +RUN --mount=type=cache,target=/$SOME_PATH dir %SOME_PATH%\foo +`, + )) dir := integration.Tmpdir( t, @@ -319,15 +373,23 @@ RUN --mount=type=cache,target=$SOME_PATH [ -f $SOME_PATH/foo ] } func testMountArg(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox ARG MNT_TYPE=cache RUN --mount=type=$MNT_TYPE,target=/mycache2 touch /mycache2/foo RUN --mount=type=cache,target=/mycache2 [ -f /mycache2/foo ] -`) +`, + ` +FROM nanoserver +USER ContainerAdministrator +ARG MNT_TYPE=cache +RUN --mount=type=$MNT_TYPE,target=/mycache2 echo hello > \mycache2\foo +RUN --mount=type=cache,target=/mycache2 dir \mycache2\foo +`, + )) dir := integration.Tmpdir( t, @@ -348,10 +410,10 @@ RUN --mount=type=cache,target=/mycache2 [ -f /mycache2/foo ] } func testMountEnvAcrossStages(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox as stage1 ENV MNT_ID=mycache @@ -361,7 +423,19 @@ RUN --mount=type=$MNT_TYPE2,id=$MNT_ID,target=/cbacba [ -f /cbacba/foo ] FROM stage1 RUN --mount=type=$MNT_TYPE2,id=$MNT_ID,target=/whatever [ -f /whatever/foo ] -`) +`, + ` +FROM nanoserver AS stage1 +USER ContainerAdministrator +ENV MNT_ID=mycache +ENV MNT_TYPE2=cache +RUN --mount=type=cache,id=mycache,target=/abcabc echo 1 > \abcabc\foo +RUN --mount=type=$MNT_TYPE2,id=$MNT_ID,target=/cbacba dir \cbacba\foo + +FROM stage1 +RUN --mount=type=$MNT_TYPE2,id=$MNT_ID,target=/whatever dir \whatever\foo +`, + )) dir := integration.Tmpdir( t, @@ -382,17 +456,27 @@ RUN --mount=type=$MNT_TYPE2,id=$MNT_ID,target=/whatever [ -f /whatever/foo ] } func testMountMetaArg(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` ARG META_PATH=/tmp/meta FROM busybox ARG META_PATH RUN --mount=type=cache,id=mycache,target=/tmp/meta touch /tmp/meta/foo RUN --mount=type=cache,id=mycache,target=$META_PATH [ -f /tmp/meta/foo ] -`) +`, + ` +ARG META_PATH=/tmp/meta + +FROM nanoserver +USER ContainerAdministrator +ARG META_PATH +RUN --mount=type=cache,id=mycache,target=/tmp/meta echo 1 > \tmp\meta\foo +RUN --mount=type=cache,id=mycache,target=$META_PATH dir \tmp\meta\foo +`, + )) dir := integration.Tmpdir( t, @@ -413,17 +497,28 @@ RUN --mount=type=cache,id=mycache,target=$META_PATH [ -f /tmp/meta/foo ] } func testMountFromError(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox as test RUN touch /tmp/test FROM busybox ENV ttt=test RUN --mount=from=$ttt,type=cache,target=/tmp ls -`) +`, + ` +FROM nanoserver AS test +RUN mkdir \tmp +RUN echo \tmp\test + +FROM nanoserver +USER ContainerAdministrator +ENV ttt=test +RUN --mount=from=$ttt,type=cache,target=/tmp dir +`, + )) dir := integration.Tmpdir( t, @@ -487,17 +582,29 @@ COPY --from=base /tmpfssize / // moby/buildkit#4123 func testMountDuplicate(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM busybox AS base RUN --mount=source=.,target=/tmp/test \ --mount=source=b.txt,target=/tmp/b.txt \ cat /tmp/test/a.txt /tmp/b.txt > /combined.txt FROM scratch COPY --from=base /combined.txt / -`) +`, + ` +FROM nanoserver AS base +USER ContainerAdministrator +RUN --mount=source=.,target=/tmp/test \ + --mount=source=b.txt,target=/tmp/b.txt \ + type \tmp\test\a.txt \tmp\b.txt > \combined.txt + +FROM nanoserver +USER ContainerAdministrator +COPY --from=base /combined.txt / +`, + )) c, err := client.New(sb.Context(), sb.Address()) require.NoError(t, err) @@ -540,10 +647,10 @@ COPY --from=base /combined.txt / // moby/buildkit#5566 func testCacheMountParallel(t *testing.T, sb integration.Sandbox) { - integration.SkipOnPlatform(t, "windows") f := getFrontend(t, sb) - dockerfile := []byte(` + dockerfile := []byte(integration.UnixOrWindows( + ` FROM alpine AS b1 RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz echo 1 @@ -553,7 +660,21 @@ RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz ec FROM scratch COPY --from=b1 /etc/passwd p1 COPY --from=b2 /etc/passwd p2 -`) +`, + ` +FROM nanoserver AS b1 +USER ContainerAdministrator +RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz echo 1 + +FROM nanoserver AS b2 +USER ContainerAdministrator +RUN --mount=type=cache,target=/foo/bar --mount=type=cache,target=/foo/bar/baz echo 2 + +FROM nanoserver +COPY --from=b1 /License.txt p1 +COPY --from=b2 /License.txt p2 +`, + )) dir := integration.Tmpdir( t, diff --git a/util/system/path_unix.go b/util/system/path_unix.go new file mode 100644 index 000000000000..55391783ff68 --- /dev/null +++ b/util/system/path_unix.go @@ -0,0 +1,17 @@ +//go:build !windows + +package system + +import "path/filepath" + +// IsAbsolutePath is just a wrapper that calls filepath.IsAbs. +// Has been added here just for symmetry with Windows. +func IsAbsolutePath(path string) bool { + return filepath.IsAbs(path) +} + +// GetAbsolutePath does nothing on non-Windows, just returns +// the same path. +func GetAbsolutePath(path string) string { + return path +} diff --git a/util/system/path_windows.go b/util/system/path_windows.go new file mode 100644 index 000000000000..726d1f2ddf0e --- /dev/null +++ b/util/system/path_windows.go @@ -0,0 +1,29 @@ +package system + +import ( + "path/filepath" + "strings" +) + +// DefaultSystemVolumeName is the default system volume label on Windows +const DefaultSystemVolumeName = "C:" + +// IsAbsolutePath prepends the default system volume label +// to the path that is presumed absolute, and then calls filepath.IsAbs +func IsAbsolutePath(path string) bool { + path = filepath.Clean(path) + if strings.HasPrefix(path, "\\") { + path = DefaultSystemVolumeName + path + } + return filepath.IsAbs(path) +} + +// GetAbsolutePath returns an absolute path rooted +// to C:\\ on Windows. +func GetAbsolutePath(path string) string { + path = filepath.Clean(path) + if len(path) >= 2 && strings.EqualFold(path[:2], DefaultSystemVolumeName) { + return path + } + return DefaultSystemVolumeName + path +}