From 7c3fd8f20fa39de8f3fb410d6fd41db5989e70f0 Mon Sep 17 00:00:00 2001 From: Ye Sijun Date: Sun, 8 May 2022 00:27:16 +0800 Subject: [PATCH] add bind-nonrecursive for mount Signed-off-by: Ye Sijun --- README.md | 35 +++---- cmd/nerdctl/run_mount_linux_test.go | 134 ++++++++++++++++++++++++++ pkg/mountutil/mountutil.go | 12 ++- pkg/mountutil/mountutil_linux.go | 40 ++++++-- pkg/mountutil/mountutil_linux_test.go | 7 ++ 5 files changed, 200 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 19ab9172ce1..4b6ca962518 100644 --- a/README.md +++ b/README.md @@ -428,33 +428,30 @@ Volume flags: Requires kernel >= 5.12, and crun >= 1.4 or runc >= 1.1 (PR [#3272](https://github.com/opencontainers/runc/pull/3272)). With older runc, `rro` just works as `ro`. - :whale: option `shared`, `slave`, `private`: Non-recursive "shared" / "slave" / "private" propagation - :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation + - :nerd_face: option `bind`: Not-recursively bind-mounted + - :nerd_face: option `rbind`: Recursively bind-mounted - :whale: `--tmpfs`: Mount a tmpfs directory, e.g. `--tmpfs /tmp:size=64m,exec`. - :whale: `--mount`: Attach a filesystem mount to the container. Consists of multiple key-value pairs, separated by commas and each consisting of a `=` tuple. e.g., `-- mount type=bind,source=/src,target=/app,bind-propagation=shared`. - - :whale: The `type` of the mount, which can be `bind`, `volume`, `tmpfs`. + - :whale: `type`: Current supported mount types are `bind`, `volume`, `tmpfs`. The defaul type will be set to `volume` if not specified. i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volum,src=vol-1,dst=/app,readonly` - - :whale: The `source` of the mount. For bind mounts, this is the path to the file - or directory. May be specified as `source` or `src`. - - :whale: The `destination` takes as its value the path where the file or directory - is mounted in the container. May be specified as `destination`, `dst`, - or `target`. - - :whale: The `readonly` or `ro`, `rw`, `rro` option changes filesystem permissinos. - See description for `--volume` for more deails. - - :whale: The `bind-propagation` option is only for `bind` mount which is used to set the - bind propagation. May be one of `rprivate`, `private`, `rshared`, `shared`, - `rslave`, `slave`. - See description for `--volume` for more deails. - - :whale: The `tmpfs-size` and `tmpfs-mode` options are only for `tmpfs` bind mount, - e.g., `--mount type=tmpfs,target=/app,tmpfs-size=10m,tmpfs-mode=1770`. - - `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default. - - `tmpfs-mode`: File mode of the tmpfs in **octal**. + - Common Options: + - :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind. + - :whale: `dst`, `destination`, `target`: Mount destination spec. + - :whale: `readonly`, `ro`, `rw`, `rro`: Filesystem permissinos. + - Options specific to `bind`: + - :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default). + - :whale: `bind-nonrecursive`: `true` or `false`(default). If set to true, submounts are not recursively bind-mounted. This option is useful for readonly bind mount. + - unimplemented options: `consistency` + - Options specific to `tmpfs`: + - :whale: `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default. + - :whale: `tmpfs-mode`: File mode of the tmpfs in **octal**. Defaults to `1777` or world-writable. - -Unimplemented `docker run --mount` flags: `bind-nonrecursive`, `volume-nocopy`, -`volume-label`, `volume-driver`, `volume-opt`, `consistency`. + - Options specific to `volume`: + - unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt` Rootfs flags: - :whale: `--read-only`: Mount the container's root filesystem as read only diff --git a/cmd/nerdctl/run_mount_linux_test.go b/cmd/nerdctl/run_mount_linux_test.go index 1db48c8fd2c..d2bc0944861 100644 --- a/cmd/nerdctl/run_mount_linux_test.go +++ b/cmd/nerdctl/run_mount_linux_test.go @@ -19,11 +19,14 @@ package main import ( "fmt" "os" + "path/filepath" "strings" "testing" "github.com/containerd/containerd/mount" + "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/testutil" + mobymount "github.com/moby/sys/mount" "gotest.tools/v3/assert" ) @@ -340,6 +343,137 @@ func TestRunBindMountBind(t *testing.T) { base.Cmd("exec", containerName, "grep", "/mnt2", "/proc/mounts").AssertOutWithFunc(f("ro")) } +func TestRunMountBindMode(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("must be superuser to use mount") + } + t.Parallel() + base := testutil.NewBase(t) + + tmpDir1, err := os.MkdirTemp(t.TempDir(), "rw") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir1) + tmpDir1Mnt := filepath.Join(tmpDir1, "mnt") + if err := os.MkdirAll(tmpDir1Mnt, 0700); err != nil { + t.Fatal(err) + } + + tmpDir2, err := os.MkdirTemp(t.TempDir(), "ro") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir2) + + if err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro"); err != nil { + t.Fatal(err) + } + defer func() { + if err := mobymount.Unmount(tmpDir1Mnt); err != nil { + t.Fatal(err) + } + }() + + base.Cmd("run", + "--rm", + "--mount", fmt.Sprintf("type=bind,bind-nonrecursive,src=%s,target=/mnt1", tmpDir1), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ).AssertOutWithFunc(func(stdout string) error { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) != 1 { + return fmt.Errorf("expected 1 line, got %q", stdout) + } + if !strings.HasPrefix(lines[0], "/mnt1") { + return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) + } + return nil + }) + + base.Cmd("run", + "--rm", + "--mount", fmt.Sprintf("type=bind,bind-nonrecursive=false,src=%s,target=/mnt1", tmpDir1), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ).AssertOutWithFunc(func(stdout string) error { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) != 2 { + return fmt.Errorf("expected 2 line, got %q", stdout) + } + if !strings.HasPrefix(lines[0], "/mnt1") { + return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) + } + return nil + }) +} + +func TestRunVolumeBindMode(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("must be superuser to use mount") + } + testutil.DockerIncompatible(t) + t.Parallel() + base := testutil.NewBase(t) + + tmpDir1, err := os.MkdirTemp(t.TempDir(), "rw") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir1) + tmpDir1Mnt := filepath.Join(tmpDir1, "mnt") + if err := os.MkdirAll(tmpDir1Mnt, 0700); err != nil { + t.Fatal(err) + } + + tmpDir2, err := os.MkdirTemp(t.TempDir(), "ro") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir2) + + if err := mobymount.Mount(tmpDir2, tmpDir1Mnt, "none", "bind,ro"); err != nil { + t.Fatal(err) + } + defer func() { + if err := mobymount.Unmount(tmpDir1Mnt); err != nil { + t.Fatal(err) + } + }() + + base.Cmd("run", + "--rm", + "-v", fmt.Sprintf("%s:/mnt1:bind", tmpDir1), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ).AssertOutWithFunc(func(stdout string) error { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) != 1 { + return fmt.Errorf("expected 1 line, got %q", stdout) + } + if !strings.HasPrefix(lines[0], "/mnt1") { + return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) + } + return nil + }) + + base.Cmd("run", + "--rm", + "-v", fmt.Sprintf("%s:/mnt1:rbind", tmpDir1), + testutil.AlpineImage, + "sh", "-euxc", "apk add findmnt -q && findmnt -nR /mnt1", + ).AssertOutWithFunc(func(stdout string) error { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + if len(lines) != 2 { + return fmt.Errorf("expected 2 line, got %q", stdout) + } + if !strings.HasPrefix(lines[0], "/mnt1") { + return fmt.Errorf("expected mount /mnt1, got %q", lines[0]) + } + return nil + }) +} + func TestRunBindMountPropagation(t *testing.T) { tID := testutil.Identifier(t) diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go index 2c9900c0b84..a84253902bb 100644 --- a/pkg/mountutil/mountutil.go +++ b/pkg/mountutil/mountutil.go @@ -121,7 +121,17 @@ func ProcessFlagV(s string, volStore volumestore.VolumeStore) (*Processed, error fstype := "nullfs" if runtime.GOOS != "freebsd" { fstype = "none" - options = append(options, "rbind") + found := false + for _, opt := range options { + switch opt { + case "rbind", "bind": + found = true + break + } + } + if !found { + options = append(options, "rbind") + } } res.Mount = specs.Mount{ Type: fstype, diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go index 95ef217b8ca..2d1848c8e5d 100644 --- a/pkg/mountutil/mountutil_linux.go +++ b/pkg/mountutil/mountutil_linux.go @@ -103,6 +103,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun var ( writeModeRawOpts []string propagationRawOpts []string + bindOpts []string ) for _, opt := range strings.Split(optsRaw, ",") { switch opt { @@ -110,6 +111,9 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun writeModeRawOpts = append(writeModeRawOpts, opt) case "private", "rprivate", "shared", "rshared", "slave", "rslave": propagationRawOpts = append(propagationRawOpts, opt) + case "bind", "rbind": + // bind means not recursively bind-mounted, rbind is the opposite + bindOpts = append(bindOpts, opt) case "": // NOP default: @@ -120,6 +124,14 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun var opts []string var specOpts []oci.SpecOpts + if len(bindOpts) > 0 && vType != Bind { + return nil, nil, fmt.Errorf("volume bind/rbind option is only supported for bind mount: %+v", bindOpts) + } else if len(bindOpts) > 1 { + return nil, nil, fmt.Errorf("duplicated bind/rbind option: %+v", bindOpts) + } else if len(bindOpts) > 0 { + opts = append(opts, bindOpts[0]) + } + if len(writeModeRawOpts) > 1 { return nil, nil, fmt.Errorf("duplicated read/write volume option: %+v", writeModeRawOpts) } else if len(writeModeRawOpts) > 0 { @@ -275,14 +287,15 @@ func ProcessFlagTmpfs(s string) (*Processed, error) { func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) { fields := strings.Split(s, ",") var ( - mountType string - src string - dst string - bindPropagation string - rwOption string - tmpfsSize int64 - tmpfsMode os.FileMode - err error + mountType string + src string + dst string + bindPropagation string + bindNonRecursive bool + rwOption string + tmpfsSize int64 + tmpfsMode os.FileMode + err error ) // set default values @@ -305,6 +318,9 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e case "readonly", "ro", "rw", "rro": rwOption = key continue + case "bind-nonrecursive": + bindNonRecursive = true + continue } } @@ -340,6 +356,11 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e // here don't validate the propagation value // parseVolumeOptions will do that. bindPropagation = value + case "bind-nonrecursive": + bindNonRecursive, err = strconv.ParseBool(value) + if err != nil { + return nil, fmt.Errorf("invalid value for %s: %s", key, value) + } case "tmpfs-size": tmpfsSize, err = units.RAMInBytes(value) if err != nil { @@ -381,6 +402,9 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e if bindPropagation != "" { options = append(options, bindPropagation) } + if mountType == Bind && bindNonRecursive { + options = append(options, "bind") + } } if len(options) > 0 { diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index ceb27143ec3..4ecf1f3f458 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -107,6 +107,13 @@ func TestParseVolumeOptions(t *testing.T) { optsRaw: "ro,private", wants: []string{"ro", "private"}, }, + { + name: "make bind nonrecursive", + vType: "bind", + src: "dummy", + optsRaw: "bind", + wants: []string{"bind", "rprivate"}, + }, { name: "make bind shared", vType: "bind",