diff --git a/client/client_test.go b/client/client_test.go index a2d2dea7..74d62c12 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -139,6 +139,7 @@ func TestIntegration(t *testing.T) { testPullZstdImage, testMergeOp, testMergeOpCache, + testRmSymlink, }, mirrors) integration.Run(t, []integration.Test{ @@ -3824,6 +3825,41 @@ func testSourceMapFromRef(t *testing.T, sb integration.Sandbox) { require.Equal(t, int32(1), srcs[0].Ranges[0].Start.Character) } +func testRmSymlink(t *testing.T, sb integration.Sandbox) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + // Test that if FileOp.Rm is called on a symlink, then + // the symlink is removed rather than the target + mnt := llb.Image("alpine"). + Run(llb.Shlex("touch /mnt/target")). + AddMount("/mnt", llb.Scratch()) + + mnt = llb.Image("alpine"). + Run(llb.Shlex("ln -s target /mnt/link")). + AddMount("/mnt", mnt) + + def, err := mnt.File(llb.Rm("link")).Marshal(sb.Context()) + require.NoError(t, err) + + destDir, err := ioutil.TempDir("", "buildkit") + require.NoError(t, err) + defer os.RemoveAll(destDir) + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterLocal, + OutputDir: destDir, + }, + }, + }, nil) + require.NoError(t, err) + + require.NoError(t, fstest.CheckDirectoryEqualWithApplier(destDir, fstest.CreateFile("target", nil, 0644))) +} + func testProxyEnv(t *testing.T, sb integration.Sandbox) { c, err := New(sb.Context(), sb.Address()) require.NoError(t, err) diff --git a/solver/llbsolver/file/backend.go b/solver/llbsolver/file/backend.go index 0060ad3f..732e6747 100644 --- a/solver/llbsolver/file/backend.go +++ b/solver/llbsolver/file/backend.go @@ -146,10 +146,16 @@ func rm(ctx context.Context, d string, action pb.FileActionRm) error { } func rmPath(root, src string, allowNotFound bool) error { - p, err := fs.RootPath(root, filepath.Join("/", src)) + src = filepath.Clean(src) + dir, base := filepath.Split(src) + if base == "" { + return errors.New("rmPath: invalid empty path") + } + dir, err := fs.RootPath(root, filepath.Join("/", dir)) if err != nil { return err } + p := filepath.Join(dir, base) if err := os.RemoveAll(p); err != nil { if errors.Is(err, os.ErrNotExist) && allowNotFound { diff --git a/solver/pb/caps.go b/solver/pb/caps.go index 5847b175..a0d54ee1 100644 --- a/solver/pb/caps.go +++ b/solver/pb/caps.go @@ -57,6 +57,7 @@ const ( CapFileBase apicaps.CapID = "file.base" CapFileRmWildcard apicaps.CapID = "file.rm.wildcard" CapFileCopyIncludeExcludePatterns apicaps.CapID = "file.copy.includeexcludepatterns" + CapFileRmNoFollowSymlink apicaps.CapID = "file.rm.nofollowsymlink" CapConstraints apicaps.CapID = "constraints" CapPlatform apicaps.CapID = "platform" @@ -328,6 +329,12 @@ func init() { Status: apicaps.CapStatusExperimental, }) + Caps.Init(apicaps.Cap{ + ID: CapFileRmNoFollowSymlink, + Enabled: true, + Status: apicaps.CapStatusExperimental, + }) + Caps.Init(apicaps.Cap{ ID: CapFileCopyIncludeExcludePatterns, Enabled: true,