Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add bind-nonrecursive for bind mount #1044

Merged
merged 1 commit into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<key>=<value>` 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
Expand Down
134 changes: 134 additions & 0 deletions cmd/nerdctl/run_mount_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)

Expand Down
12 changes: 11 additions & 1 deletion pkg/mountutil/mountutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 32 additions & 8 deletions pkg/mountutil/mountutil_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,17 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun
var (
writeModeRawOpts []string
propagationRawOpts []string
bindOpts []string
)
for _, opt := range strings.Split(optsRaw, ",") {
switch opt {
case "rw", "ro", "rro":
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:
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions pkg/mountutil/mountutil_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down