From f2531c938435dadcd87420b2baa17426eea77ac2 Mon Sep 17 00:00:00 2001 From: Jiwoo Ahn Date: Fri, 7 Aug 2026 18:41:27 +0900 Subject: [PATCH] feat: add volume-nocopy Signed-off-by: Jiwoo Ahn --- .../container/container_run_mount_linux_test.go | 14 ++++++++++++++ docs/command-reference.md | 3 ++- pkg/cmd/container/run_mount.go | 2 +- pkg/mountutil/mountutil.go | 1 + pkg/mountutil/mountutil_linux.go | 9 +++++++++ pkg/mountutil/mountutil_linux_test.go | 13 +++++++++++++ 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index 76a204588c1..10e21052c69 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -234,8 +234,12 @@ CMD ["cat", "/mnt/initial_file"] volName := data.Identifier("vol") helpers.Ensure("volume", "create", volName) + noCopyVolName := data.Identifier("nocopy-vol") + helpers.Ensure("volume", "create", noCopyVolName) + data.Labels().Set("img", imgName) data.Labels().Set("vol", volName) + data.Labels().Set("nocopy-vol", noCopyVolName) } testCase.SubTests = []*test.Case{ @@ -263,12 +267,22 @@ CMD ["cat", "/mnt/initial_file"] }, Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hi\n")), }, + { + Description: "with volume-nocopy", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + mount := fmt.Sprintf("type=volume,source=%s,target=/mnt,volume-nocopy", data.Labels().Get("nocopy-vol")) + return helpers.Command("run", "--rm", "--mount", mount, data.Labels().Get("img"), "sh", "-c", "test ! -e /mnt/initial_file") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", data.Labels().Get("vol")) helpers.Anyhow("rmi", data.Labels().Get("img")) helpers.Anyhow("builder", "prune", "--all", "--force") + helpers.Anyhow("volume", "rm", data.Labels().Get("nocopy-vol")) } testCase.Run(t) diff --git a/docs/command-reference.md b/docs/command-reference.md index 20239a0f3b0..d8956284123 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -324,7 +324,8 @@ Volume flags: - :whale: `tmpfs-mode`: File mode of the tmpfs in **octal**. Defaults to `1777` or world-writable. - Options specific to `volume`: - - unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt` + - :whale: `volume-nocopy`: Do not copy existing data from the container into the volume. + - unimplemented options: `volume-label`, `volume-driver`, `volume-opt` - Options specific to `image`: - :whale: `src`, `source`: image reference (mandatory). - :whale: Currently, the image filesystem is mounted read-only. diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index 34ab8fa1230..d6bdb97e603 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -319,7 +319,7 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm } // Copying content in AnonymousVolume and namedVolume - if x.Type == "volume" { + if x.Type == mountutil.Volume && !x.VolumeNoCopy { if err := copyExistingContents(target, x.Mount.Source); err != nil { return nil, nil, nil, err } diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go index f4ce3bd8f1d..350d4443376 100644 --- a/pkg/mountutil/mountutil.go +++ b/pkg/mountutil/mountutil.go @@ -51,6 +51,7 @@ type Processed struct { AnonymousVolume string // anonymous volume name Mode string Opts []oci.SpecOpts + VolumeNoCopy bool // ImageMountSnapshot is the snapshotter key of the read-only view for a // type=image mount; empty for other mount types. ImageMountSnapshot string diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 02da626213b..564721d6d05 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -369,6 +369,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str bindPropagation string bindNonRecursive bool bindRecursive string // "enabled", "disabled", "writable", or "readonly" + volumeNoCopy bool rwOption string tmpfsSize int64 tmpfsMode os.FileMode @@ -404,6 +405,9 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str log.L.Warn("The mount option \"bind-nonrecursive\" is deprecated; use \"bind-recursive=disabled\" instead") bindNonRecursive = true continue + case "volume-nocopy": + volumeNoCopy = true + continue } } @@ -480,6 +484,10 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str } } + if volumeNoCopy && mountType != Volume { + return nil, fmt.Errorf("the option 'volume-nocopy' is only supported for volume mounts") + } + // type=image's source is an image reference resolved later with a containerd // client; validate the intent here. Like Docker, an image mount is always // read-only: a readonly/ro option is accepted for compatibility but the @@ -593,6 +601,7 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore, ociRuntime str if err != nil { return nil, err } + res.VolumeNoCopy = volumeNoCopy if rwOption != "" { roOpts, err := readOnlyMountOptions(roMode, ociRuntime) if err != nil { diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 5ee383d7065..62450da123d 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -665,3 +665,16 @@ func TestProcessFlagMountImage(t *testing.T) { }) } } + +func TestProcessFlagMountVolumeNoCopy(t *testing.T) { + got, err := ProcessFlagMount( + "type=volume,source=TestVolume,target=/mnt,volume-nocopy", + mockVolumeStore, + "", + ) + assert.NilError(t, err) + + assert.Equal(t, got.Type, Volume) + assert.Equal(t, got.Name, "TestVolume") + assert.Assert(t, got.VolumeNoCopy) +}