Skip to content

Commit 34d5878

Browse files
committed
Use mount.Target to specify subdirectory of rootfs mount
- Add Target to mount.Mount. - Add UnmountMounts to unmount a list of mounts in reverse order. - Add UnmountRecursive to unmount deepest mount first for a given target, using moby/sys/mountinfo. Signed-off-by: Edgar Lee <[email protected]>
1 parent 753bfd6 commit 34d5878

26 files changed

Lines changed: 284 additions & 67 deletions

File tree

cmd/ctr/commands/snapshots/snapshots.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"io"
2424
"os"
25+
"path/filepath"
2526
"strings"
2627
"text/tabwriter"
2728
"time"
@@ -640,6 +641,6 @@ func printNode(name string, tree *snapshotTree, level int) {
640641
func printMounts(target string, mounts []mount.Mount) {
641642
// FIXME: This is specific to Unix
642643
for _, m := range mounts {
643-
fmt.Printf("mount -t %s %s %s -o %s\n", m.Type, m.Source, target, strings.Join(m.Options, ","))
644+
fmt.Printf("mount -t %s %s %s -o %s\n", m.Type, m.Source, filepath.Join(target, m.Target), strings.Join(m.Options, ","))
644645
}
645646
}

container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ func (c *container) NewTask(ctx context.Context, ioCreate cio.Creator, opts ...N
258258
request.Rootfs = append(request.Rootfs, &types.Mount{
259259
Type: m.Type,
260260
Source: m.Source,
261+
Target: m.Target,
261262
Options: m.Options,
262263
})
263264
}
@@ -275,6 +276,7 @@ func (c *container) NewTask(ctx context.Context, ioCreate cio.Creator, opts ...N
275276
request.Rootfs = append(request.Rootfs, &types.Mount{
276277
Type: m.Type,
277278
Source: m.Source,
279+
Target: m.Target,
278280
Options: m.Options,
279281
})
280282
}

contrib/snapshotservice/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ func fromMounts(mounts []mount.Mount) []*types.Mount {
205205
out[i] = &types.Mount{
206206
Type: m.Type,
207207
Source: m.Source,
208+
Target: m.Target,
208209
Options: m.Options,
209210
}
210211
}

diff.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ func fromMounts(mounts []mount.Mount) []*types.Mount {
128128
apiMounts[i] = &types.Mount{
129129
Type: m.Type,
130130
Source: m.Source,
131+
Target: m.Target,
131132
Options: m.Options,
132133
}
133134
}

docs/snapshotters/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,37 @@ Filesystem-specific:
2424
- `nydus`: [Nydus Snapshotter](https://github.com/containerd/nydus-snapshotter)
2525
- `overlaybd`: [OverlayBD Snapshotter](https://github.com/containerd/accelerated-container-image)
2626
- `stargz`: [Stargz Snapshotter](https://github.com/containerd/stargz-snapshotter)
27+
28+
## Mount target
29+
30+
Mounts can optionally specify a target to describe submounts in the container's
31+
rootfs. For example, if the snapshotter wishes to bind mount to a subdirectory
32+
ontop of an overlayfs mount, they can return the following mounts:
33+
34+
```json
35+
[
36+
{
37+
"type": "overlay",
38+
"source": "overlay",
39+
"options": [
40+
"workdir=...",
41+
"upperdir=...",
42+
"lowerdir=..."
43+
]
44+
},
45+
{
46+
"type": "bind",
47+
"source": "/path/on/host",
48+
"target": "/path/inside/container",
49+
"options": [
50+
"ro",
51+
"rbind"
52+
]
53+
}
54+
]
55+
```
56+
57+
However, the mountpoint `/path/inside/container` needs to exist for the bind
58+
mount, so one of the previous mounts must be responsible for providing that
59+
directory in the rootfs. In this case, one of the lower dirs of the overlay has
60+
that directory to enable the bind mount.

mount/mount.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616

1717
package mount
1818

19+
import (
20+
"fmt"
21+
22+
"github.com/containerd/continuity/fs"
23+
)
24+
1925
// Mount is the lingua franca of containerd. A mount represents a
2026
// serialized mount syscall. Components either emit or consume mounts.
2127
type Mount struct {
@@ -24,12 +30,16 @@ type Mount struct {
2430
// Source specifies where to mount from. Depending on the host system, this
2531
// can be a source path or device.
2632
Source string
33+
// Target specifies an optional subdirectory as a mountpoint. It assumes that
34+
// the subdirectory exists in a parent mount.
35+
Target string
2736
// Options contains zero or more fstab-style mount options. Typically,
2837
// these are platform specific.
2938
Options []string
3039
}
3140

32-
// All mounts all the provided mounts to the provided target
41+
// All mounts all the provided mounts to the provided target. If submounts are
42+
// present, it assumes that parent mounts come before child mounts.
3343
func All(mounts []Mount, target string) error {
3444
for _, m := range mounts {
3545
if err := m.Mount(target); err != nil {
@@ -38,3 +48,30 @@ func All(mounts []Mount, target string) error {
3848
}
3949
return nil
4050
}
51+
52+
// UnmountMounts unmounts all the mounts under a target in the reverse order of
53+
// the mounts array provided.
54+
func UnmountMounts(mounts []Mount, target string, flags int) error {
55+
for i := len(mounts) - 1; i >= 0; i-- {
56+
mountpoint, err := fs.RootPath(target, mounts[i].Target)
57+
if err != nil {
58+
return err
59+
}
60+
61+
if err := UnmountAll(mountpoint, flags); err != nil {
62+
if i == len(mounts)-1 { // last mount
63+
return err
64+
}
65+
}
66+
}
67+
return nil
68+
}
69+
70+
// Mount to the provided target path.
71+
func (m *Mount) Mount(target string) error {
72+
target, err := fs.RootPath(target, m.Target)
73+
if err != nil {
74+
return fmt.Errorf("failed to join path %q with root %q: %w", m.Target, target, err)
75+
}
76+
return m.mount(target)
77+
}

mount/mount_freebsd.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,12 @@ var (
3131
ErrNotImplementOnUnix = errors.New("not implemented under unix")
3232
)
3333

34-
// Mount to the provided target path.
35-
func (m *Mount) Mount(target string) error {
36-
// The "syscall" and "golang.org/x/sys/unix" packages do not define a Mount
37-
// function for FreeBSD, so instead we execute mount(8) and trust it to do
38-
// the right thing
39-
return m.mountWithHelper(target)
40-
}
41-
42-
func (m *Mount) mountWithHelper(target string) error {
34+
// Mount to the provided target.
35+
//
36+
// The "syscall" and "golang.org/x/sys/unix" packages do not define a Mount
37+
// function for FreeBSD, so instead we execute mount(8) and trust it to do
38+
// the right thing
39+
func (m *Mount) mount(target string) error {
4340
// target: "/foo/target"
4441
// command: "mount -o ro -t nullfs /foo/source /foo/merged"
4542
// Note: FreeBSD mount(8) is particular about the order of flags and arguments

mount/mount_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func init() {
4242
//
4343
// If m.Type starts with "fuse." or "fuse3.", "mount.fuse" or "mount.fuse3"
4444
// helper binary is called.
45-
func (m *Mount) Mount(target string) (err error) {
45+
func (m *Mount) mount(target string) (err error) {
4646
for _, helperBinary := range allowedHelperBinaries {
4747
// helperBinary = "mount.fuse", typePrefix = "fuse."
4848
typePrefix := strings.TrimPrefix(helperBinary, "mount.") + "."

mount/mount_linux_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,67 @@ func TestMountAt(t *testing.T) {
172172
t.Fatalf("unexpected working directory: %s", newWD)
173173
}
174174
}
175+
176+
func TestUnmountMounts(t *testing.T) {
177+
testutil.RequiresRoot(t)
178+
179+
target, mounts := setupMounts(t)
180+
if err := UnmountMounts(mounts, target, 0); err != nil {
181+
t.Fatal(err)
182+
}
183+
}
184+
185+
func TestUnmountRecursive(t *testing.T) {
186+
testutil.RequiresRoot(t)
187+
188+
target, _ := setupMounts(t)
189+
if err := UnmountRecursive(target, 0); err != nil {
190+
t.Fatal(err)
191+
}
192+
}
193+
194+
func setupMounts(t *testing.T) (target string, mounts []Mount) {
195+
dir1 := t.TempDir()
196+
dir2 := t.TempDir()
197+
198+
if err := os.Mkdir(filepath.Join(dir1, "foo"), 0755); err != nil {
199+
t.Fatal(err)
200+
}
201+
mounts = append(mounts, Mount{
202+
Type: "bind",
203+
Source: dir1,
204+
Options: []string{
205+
"ro",
206+
"rbind",
207+
},
208+
})
209+
210+
if err := os.WriteFile(filepath.Join(dir2, "bar"), []byte("bar"), 0644); err != nil {
211+
t.Fatal(err)
212+
}
213+
mounts = append(mounts, Mount{
214+
Type: "bind",
215+
Source: dir2,
216+
Target: "foo",
217+
Options: []string{
218+
"ro",
219+
"rbind",
220+
},
221+
})
222+
223+
target = t.TempDir()
224+
if err := All(mounts, target); err != nil {
225+
t.Fatal(err)
226+
}
227+
228+
b, err := os.ReadFile(filepath.Join(target, "foo/bar"))
229+
if err != nil {
230+
t.Fatal(err)
231+
}
232+
233+
if string(b) != "bar" {
234+
t.Fatalf("unexpected file content: %s", b)
235+
}
236+
237+
return target, mounts
238+
}

mount/mount_unix.go

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build darwin || openbsd
1+
//go:build !windows && !darwin && !openbsd
22

33
/*
44
Copyright The containerd Authors.
@@ -18,24 +18,41 @@
1818

1919
package mount
2020

21-
import "errors"
21+
import (
22+
"sort"
2223

23-
var (
24-
// ErrNotImplementOnUnix is returned for methods that are not implemented
25-
ErrNotImplementOnUnix = errors.New("not implemented under unix")
24+
"github.com/moby/sys/mountinfo"
2625
)
2726

28-
// Mount is not implemented on this platform
29-
func (m *Mount) Mount(target string) error {
30-
return ErrNotImplementOnUnix
31-
}
32-
33-
// Unmount is not implemented on this platform
34-
func Unmount(mount string, flags int) error {
35-
return ErrNotImplementOnUnix
36-
}
37-
38-
// UnmountAll is not implemented on this platform
39-
func UnmountAll(mount string, flags int) error {
40-
return ErrNotImplementOnUnix
27+
// UnmountRecursive unmounts the target and all mounts underneath, starting
28+
// with the deepest mount first.
29+
func UnmountRecursive(target string, flags int) error {
30+
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(target))
31+
if err != nil {
32+
return err
33+
}
34+
35+
targetSet := make(map[string]struct{})
36+
for _, m := range mounts {
37+
targetSet[m.Mountpoint] = struct{}{}
38+
}
39+
40+
var targets []string
41+
for m := range targetSet {
42+
targets = append(targets, m)
43+
}
44+
45+
// Make the deepest mount be first
46+
sort.SliceStable(targets, func(i, j int) bool {
47+
return len(targets[i]) > len(targets[j])
48+
})
49+
50+
for i, target := range targets {
51+
if err := UnmountAll(target, flags); err != nil {
52+
if i == len(targets)-1 { // last mount
53+
return err
54+
}
55+
}
56+
}
57+
return nil
4158
}

0 commit comments

Comments
 (0)