Skip to content

Commit b1265f1

Browse files
authored
Merge pull request #801 from liubin/f/713-add-mount-option
add --mount option for nerdctl run
2 parents 655f213 + 75ac127 commit b1265f1

File tree

7 files changed

+445
-4
lines changed

7 files changed

+445
-4
lines changed

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,33 @@ Volume flags:
384384
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`.
385385
- :whale: option `shared`, `slave`, `private`: Non-recursive "shared" / "slave" / "private" propagation
386386
- :whale: option `rshared`, `rslave`, `rprivate`: Recursive "shared" / "slave" / "private" propagation
387-
- :whale: `--tmpfs`: Mount a tmpfs directory
387+
- :whale: `--tmpfs`: Mount a tmpfs directory, e.g. `--tmpfs /tmp:size=64m,exec`.
388+
- :whale: `--mount`: Attach a filesystem mount to the container.
389+
Consists of multiple key-value pairs, separated by commas and each
390+
consisting of a `<key>=<value>` tuple.
391+
e.g., `-- mount type=bind,source=/src,target=/app,bind-propagation=shared`.
392+
- :whale: The `type` of the mount, which can be `bind`, `volume`, `tmpfs`.
393+
The defaul type will be set to `volume` if not specified.
394+
i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volum,src=vol-1,dst=/app,readonly`
395+
- :whale: The `source` of the mount. For bind mounts, this is the path to the file
396+
or directory. May be specified as `source` or `src`.
397+
- :whale: The `destination` takes as its value the path where the file or directory
398+
is mounted in the container. May be specified as `destination`, `dst`,
399+
or `target`.
400+
- :whale: The `readonly` or `ro`, `rw`, `rro` option changes filesystem permissinos.
401+
See description for `--volume` for more deails.
402+
- :whale: The `bind-propagation` option is only for `bind` mount which is used to set the
403+
bind propagation. May be one of `rprivate`, `private`, `rshared`, `shared`,
404+
`rslave`, `slave`.
405+
See description for `--volume` for more deails.
406+
- :whale: The `tmpfs-size` and `tmpfs-mode` options are only for `tmpfs` bind mount,
407+
e.g., `--mount type=tmpfs,target=/app,tmpfs-size=10m,tmpfs-mode=1770`.
408+
- `tmpfs-size`: Size of the tmpfs mount in bytes. Unlimited by default.
409+
- `tmpfs-mode`: File mode of the tmpfs in **octal**.
410+
Defaults to `1777` or world-writable.
411+
412+
Unimplemented `docker run --mount` flags: `bind-nonrecursive`, `volume-nocopy`,
413+
`volume-label`, `volume-driver`, `volume-opt`, `consistency`.
388414

389415
Rootfs flags:
390416
- :whale: `--read-only`: Mount the container's root filesystem as read only

cmd/nerdctl/run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ func setCreateFlags(cmd *cobra.Command) {
189189
cmd.Flags().StringArrayP("volume", "v", nil, "Bind mount a volume")
190190
// tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"}
191191
cmd.Flags().StringArray("tmpfs", nil, "Mount a tmpfs directory")
192+
cmd.Flags().StringArray("mount", nil, "Attach a filesystem mount to the container")
192193
// #endregion
193194

194195
// rootfs flags

cmd/nerdctl/run_mount.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ func withMounts(mounts []specs.Mount) oci.SpecOpts {
7575
}
7676
}
7777

78-
// parseMountFlags parses --volume and --tmpfs.
79-
// parseMountFlags will also parse --mount in a future release.
78+
// parseMountFlags parses --volume, --mount and --tmpfs.
8079
func parseMountFlags(cmd *cobra.Command, volStore volumestore.VolumeStore) ([]*mountutil.Processed, error) {
8180
var parsed []*mountutil.Processed
8281
if flagVSlice, err := cmd.Flags().GetStringArray("volume"); err != nil {
@@ -103,6 +102,19 @@ func parseMountFlags(cmd *cobra.Command, volStore volumestore.VolumeStore) ([]*m
103102
parsed = append(parsed, x)
104103
}
105104
}
105+
106+
if mountsSlice, err := cmd.Flags().GetStringArray("mount"); err != nil {
107+
return nil, err
108+
} else {
109+
for _, v := range strutil.DedupeStrSlice(mountsSlice) {
110+
x, err := mountutil.ProcessFlagMount(v, volStore)
111+
if err != nil {
112+
return nil, err
113+
}
114+
parsed = append(parsed, x)
115+
}
116+
}
117+
106118
return parsed, nil
107119
}
108120

@@ -205,7 +217,7 @@ func generateMountOpts(cmd *cobra.Command, ctx context.Context, client *containe
205217
return nil, nil, err
206218
}
207219

208-
//Copying content in AnonymousVolume and namedVolume
220+
// Copying content in AnonymousVolume and namedVolume
209221
if x.Type == "volume" {
210222
if err := copyExistingContents(target, x.Mount.Source); err != nil {
211223
return nil, nil, err

cmd/nerdctl/run_mount_linux_test.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"strings"
2323
"testing"
2424

25+
"github.com/containerd/containerd/mount"
2526
"github.com/containerd/nerdctl/pkg/testutil"
2627
"gotest.tools/v3/assert"
2728
)
@@ -248,3 +249,230 @@ func TestRunTmpfs(t *testing.T) {
248249
// for https://github.com/containerd/nerdctl/issues/594
249250
base.Cmd("run", "--rm", "--tmpfs", "/dev/shm:rw,exec,size=1g", testutil.AlpineImage, "grep", "/dev/shm", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=1048576k"}, []string{"noexec"}))
250251
}
252+
253+
func TestRunBindMountTmpfs(t *testing.T) {
254+
t.Parallel()
255+
base := testutil.NewBase(t)
256+
f := func(allow []string) func(stdout string) error {
257+
return func(stdout string) error {
258+
lines := strings.Split(strings.TrimSpace(stdout), "\n")
259+
if len(lines) != 1 {
260+
return fmt.Errorf("expected 1 lines, got %q", stdout)
261+
}
262+
for _, s := range allow {
263+
if !strings.Contains(stdout, s) {
264+
return fmt.Errorf("expected stdout to contain %q, got %q", s, stdout)
265+
}
266+
}
267+
return nil
268+
}
269+
}
270+
base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "noexec"}))
271+
base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp,tmpfs-size=64m", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=65536k"}))
272+
}
273+
274+
func TestRunBindMountBind(t *testing.T) {
275+
t.Parallel()
276+
base := testutil.NewBase(t)
277+
tID := testutil.Identifier(t)
278+
rwDir, err := os.MkdirTemp(t.TempDir(), "rw")
279+
if err != nil {
280+
t.Fatal(err)
281+
}
282+
roDir, err := os.MkdirTemp(t.TempDir(), "ro")
283+
if err != nil {
284+
t.Fatal(err)
285+
}
286+
287+
containerName := tID
288+
defer base.Cmd("rm", "-f", containerName).Run()
289+
base.Cmd("run",
290+
"-d",
291+
"--name", containerName,
292+
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir),
293+
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt2,ro", roDir),
294+
testutil.AlpineImage,
295+
"top",
296+
).AssertOK()
297+
base.Cmd("exec", containerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK()
298+
base.Cmd("exec", containerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail()
299+
300+
base.Cmd("run",
301+
"--rm",
302+
"--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir),
303+
testutil.AlpineImage,
304+
"cat", "/mnt1/file1",
305+
).AssertOutExactly("str1")
306+
307+
// check `bind-propagation`
308+
f := func(allow string) func(stdout string) error {
309+
return func(stdout string) error {
310+
lines := strings.Split(strings.TrimSpace(stdout), "\n")
311+
if len(lines) != 1 {
312+
return fmt.Errorf("expected 1 lines, got %q", stdout)
313+
}
314+
fields := strings.Split(lines[0], " ")
315+
if len(fields) < 4 {
316+
return fmt.Errorf("invalid /proc/mounts format %q", stdout)
317+
}
318+
319+
options := strings.Split(fields[3], ",")
320+
321+
found := false
322+
for _, s := range options {
323+
if allow == s {
324+
found = true
325+
break
326+
}
327+
}
328+
if !found {
329+
return fmt.Errorf("expected stdout to contain %q, got %+v", allow, options)
330+
}
331+
return nil
332+
}
333+
}
334+
base.Cmd("exec", containerName, "grep", "/mnt1", "/proc/mounts").AssertOutWithFunc(f("rw"))
335+
base.Cmd("exec", containerName, "grep", "/mnt2", "/proc/mounts").AssertOutWithFunc(f("ro"))
336+
}
337+
338+
func TestRunBindMountPropagation(t *testing.T) {
339+
tID := testutil.Identifier(t)
340+
341+
if !isRootfsShareableMount() {
342+
t.Skipf("rootfs doesn't support shared mount, skip test %s", tID)
343+
}
344+
345+
t.Parallel()
346+
base := testutil.NewBase(t)
347+
348+
testCases := []struct {
349+
propagation string
350+
assertFunc func(containerName, containerNameReplica string)
351+
}{
352+
{
353+
propagation: "rshared",
354+
assertFunc: func(containerName, containerNameReplica string) {
355+
// replica can get sub-mounts from original
356+
base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica")
357+
358+
// and sub-mounts from replica will be propagated to the original too
359+
base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertOutExactly("fromreplica")
360+
},
361+
},
362+
{
363+
propagation: "rslave",
364+
assertFunc: func(containerName, containerNameReplica string) {
365+
// replica can get sub-mounts from original
366+
base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica")
367+
368+
// but sub-mounts from replica will not be propagated to the original
369+
base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail()
370+
},
371+
},
372+
{
373+
propagation: "rprivate",
374+
assertFunc: func(containerName, containerNameReplica string) {
375+
// replica can't get sub-mounts from original
376+
base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertFail()
377+
// and sub-mounts from replica will not be propagated to the original too
378+
base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail()
379+
},
380+
},
381+
{
382+
propagation: "",
383+
assertFunc: func(containerName, containerNameReplica string) {
384+
// replica can't get sub-mounts from original
385+
base.Cmd("exec", containerNameReplica, "cat", "/mnt1/replica/foo.txt").AssertFail()
386+
// and sub-mounts from replica will not be propagated to the original too
387+
base.Cmd("exec", containerName, "cat", "/mnt1/bar/bar.txt").AssertFail()
388+
},
389+
},
390+
}
391+
392+
for _, tc := range testCases {
393+
propagationName := tc.propagation
394+
if propagationName == "" {
395+
propagationName = "default"
396+
}
397+
398+
t.Logf("Running test propagation case %s", propagationName)
399+
400+
rwDir, err := os.MkdirTemp(t.TempDir(), "rw")
401+
if err != nil {
402+
t.Fatal(err)
403+
}
404+
405+
containerName := tID + "-" + propagationName
406+
containerNameReplica := containerName + "-replica"
407+
408+
mountOption := fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=%s", rwDir, tc.propagation)
409+
if tc.propagation == "" {
410+
mountOption = fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir)
411+
}
412+
413+
containers := []struct {
414+
name string
415+
mountOption string
416+
}{
417+
{
418+
name: containerName,
419+
mountOption: fmt.Sprintf("type=bind,src=%s,target=/mnt1,bind-propagation=rshared", rwDir),
420+
},
421+
{
422+
name: containerNameReplica,
423+
mountOption: mountOption,
424+
},
425+
}
426+
for _, c := range containers {
427+
base.Cmd("run", "-d",
428+
"--privileged",
429+
"--name", c.name,
430+
"--mount", c.mountOption,
431+
testutil.AlpineImage,
432+
"top").AssertOK()
433+
defer base.Cmd("rm", "-f", c.name).Run()
434+
}
435+
436+
// mount in the first container
437+
base.Cmd("exec", containerName, "sh", "-exc", "mkdir /app && mkdir /mnt1/replica && mount --bind /app /mnt1/replica && echo -n toreplica > /app/foo.txt").AssertOK()
438+
base.Cmd("exec", containerName, "cat", "/mnt1/replica/foo.txt").AssertOutExactly("toreplica")
439+
440+
// mount in the second container
441+
base.Cmd("exec", containerNameReplica, "sh", "-exc", "mkdir /bar && mkdir /mnt1/bar").AssertOK()
442+
base.Cmd("exec", containerNameReplica, "sh", "-exc", "mount --bind /bar /mnt1/bar").AssertOK()
443+
444+
base.Cmd("exec", containerNameReplica, "sh", "-exc", "echo -n fromreplica > /bar/bar.txt").AssertOK()
445+
base.Cmd("exec", containerNameReplica, "cat", "/mnt1/bar/bar.txt").AssertOutExactly("fromreplica")
446+
447+
// call case specific assert function
448+
tc.assertFunc(containerName, containerNameReplica)
449+
450+
// umount mount point in the first privileged container
451+
base.Cmd("exec", containerNameReplica, "sh", "-exc", "umount /mnt1/bar").AssertOK()
452+
base.Cmd("exec", containerName, "sh", "-exc", "umount /mnt1/replica").AssertOK()
453+
}
454+
}
455+
456+
// isRootfsShareableMount will check if /tmp or / support shareable mount
457+
func isRootfsShareableMount() bool {
458+
existFunc := func(mi mount.Info) bool {
459+
for _, opt := range strings.Split(mi.Optional, " ") {
460+
if strings.HasPrefix(opt, "shared:") {
461+
return true
462+
}
463+
}
464+
return false
465+
}
466+
467+
mi, err := mount.Lookup("/tmp")
468+
if err == nil {
469+
return existFunc(mi)
470+
}
471+
472+
mi, err = mount.Lookup("/")
473+
if err == nil {
474+
return existFunc(mi)
475+
}
476+
477+
return false
478+
}

pkg/mountutil/mountutil_freebsd.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/containerd/containerd/errdefs"
2424
"github.com/containerd/containerd/oci"
25+
"github.com/containerd/nerdctl/pkg/mountutil/volumestore"
2526
"github.com/sirupsen/logrus"
2627
)
2728

@@ -61,3 +62,7 @@ func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, e
6162
func ProcessFlagTmpfs(s string) (*Processed, error) {
6263
return nil, errdefs.ErrNotImplemented
6364
}
65+
66+
func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) {
67+
return nil, errdefs.ErrNotImplemented
68+
}

0 commit comments

Comments
 (0)