diff --git a/core/diff/apply/apply_darwin.go b/core/diff/apply/apply_darwin.go deleted file mode 100644 index d3d7ec79f370..000000000000 --- a/core/diff/apply/apply_darwin.go +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package apply - -import ( - "context" - "io" - "os" - - "github.com/containerd/containerd/v2/core/mount" - "github.com/containerd/containerd/v2/pkg/archive" -) - -func apply(ctx context.Context, mounts []mount.Mount, r io.Reader, _sync bool) error { - // We currently do not support mounts nor bind mounts on MacOS in the containerd daemon. - // Using this as an exception to enable native snapshotter and allow further research. - if len(mounts) == 1 && mounts[0].Type == "bind" { - opts := []archive.ApplyOpt{} - - if os.Getuid() != 0 { - opts = append(opts, archive.WithNoSameOwner()) - } - - path := mounts[0].Source - _, err := archive.Apply(ctx, path, r, opts...) - return err - - // TODO: Do we need to sync all the filesystems? - } - - return mount.WithTempMount(ctx, mounts, func(root string) error { - _, err := archive.Apply(ctx, root, r) - return err - }) -} diff --git a/core/diff/apply/apply_other.go b/core/diff/apply/apply_other.go index af5220453a67..fc37d9234e9c 100644 --- a/core/diff/apply/apply_other.go +++ b/core/diff/apply/apply_other.go @@ -1,4 +1,4 @@ -//go:build !linux && !darwin +//go:build !linux /* Copyright The containerd Authors. @@ -21,6 +21,7 @@ package apply import ( "context" "io" + "os" "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/containerd/v2/pkg/archive" @@ -28,6 +29,21 @@ import ( func apply(ctx context.Context, mounts []mount.Mount, r io.Reader, _sync bool) error { // TODO: for windows, how to sync? + + if !mount.HasBindMounts && len(mounts) == 1 && mounts[0].Type == "bind" { + opts := []archive.ApplyOpt{} + + if os.Getuid() != 0 { + opts = append(opts, archive.WithNoSameOwner()) + } + + path := mounts[0].Source + _, err := archive.Apply(ctx, path, r, opts...) + return err + + // TODO: Do we need to sync all the filesystems? + } + return mount.WithTempMount(ctx, mounts, func(root string) error { _, err := archive.Apply(ctx, root, r) return err diff --git a/core/mount/fuse_linux.go b/core/mount/fuse_linux.go new file mode 100644 index 000000000000..b3a32b682b7c --- /dev/null +++ b/core/mount/fuse_linux.go @@ -0,0 +1,50 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mount + +import ( + "os/exec" + + "golang.org/x/sys/unix" +) + +// fuseSuperMagic is defined in statfs(2) +const fuseSuperMagic = 0x65735546 + +func isFUSE(dir string) bool { + var st unix.Statfs_t + if err := unix.Statfs(dir, &st); err != nil { + return false + } + return st.Type == fuseSuperMagic +} + +// unmountFUSE attempts to unmount using fusermount/fusermount3 helper binary. +// +// For FUSE mounts, using these helper binaries is preferred, see: +// https://github.com/containerd/containerd/pull/3765#discussion_r342083514 +func unmountFUSE(target string) error { + var err error + for _, helperBinary := range []string{"fusermount3", "fusermount"} { + cmd := exec.Command(helperBinary, "-u", target) + err = cmd.Run() + if err == nil { + return nil + } + } + return err +} diff --git a/core/mount/fuse_unsupported.go b/core/mount/fuse_unsupported.go new file mode 100644 index 000000000000..04de88c2c54f --- /dev/null +++ b/core/mount/fuse_unsupported.go @@ -0,0 +1,30 @@ +//go:build !linux && !windows + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mount + +import "fmt" + +func isFUSE(dir string) bool { + return false +} + +// unmountFUSE is not implemented on this platform +func unmountFUSE(target string) error { + return fmt.Errorf("FUSE is not supported on this platform") +} diff --git a/core/mount/mount.go b/core/mount/mount.go index 16865b4f443b..e8e0fe31c086 100644 --- a/core/mount/mount.go +++ b/core/mount/mount.go @@ -19,12 +19,16 @@ package mount import ( "fmt" "path/filepath" + "runtime" "strings" "github.com/containerd/containerd/api/types" "github.com/containerd/continuity/fs" ) +// HasBindMounts This is a flag to conditionally disable code that relies on working bind-mount support, so such code is easier to find across codebase. +const HasBindMounts = runtime.GOOS != "darwin" && runtime.GOOS != "openbsd" + // Mount is the lingua franca of containerd. A mount represents a // serialized mount syscall. Components either emit or consume mounts. type Mount struct { diff --git a/core/mount/mount_darwin.go b/core/mount/mount_darwin.go new file mode 100644 index 000000000000..f319d69707d0 --- /dev/null +++ b/core/mount/mount_darwin.go @@ -0,0 +1,24 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mount + +import "github.com/containerd/errdefs" + +// Mount to the provided target. +func (m *Mount) mount(target string) error { + return errdefs.ErrNotImplemented +} diff --git a/core/mount/mount_freebsd.go b/core/mount/mount_freebsd.go index 8b384267fe4f..8dc04d5d6f7b 100644 --- a/core/mount/mount_freebsd.go +++ b/core/mount/mount_freebsd.go @@ -19,18 +19,11 @@ package mount import ( "errors" "fmt" - "os" "os/exec" - "time" "golang.org/x/sys/unix" ) -var ( - // ErrNotImplementOnUnix is returned for methods that are not implemented - ErrNotImplementOnUnix = errors.New("not implemented under unix") -) - // Mount to the provided target. // // The "syscall" and "golang.org/x/sys/unix" packages do not define a Mount @@ -77,57 +70,3 @@ func (m *Mount) mount(target string) error { } return fmt.Errorf("mount [%v] failed with ECHILD (retried %d times)", args, retriesOnECHILD) } - -// Unmount the provided mount path with the flags -func Unmount(target string, flags int) error { - if err := unmount(target, flags); err != nil && err != unix.EINVAL { - return err - } - return nil -} - -func unmount(target string, flags int) error { - for i := 0; i < 50; i++ { - if err := unix.Unmount(target, flags); err != nil { - switch err { - case unix.EBUSY: - time.Sleep(50 * time.Millisecond) - continue - default: - return err - } - } - return nil - } - return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY) -} - -// UnmountAll repeatedly unmounts the given mount point until there -// are no mounts remaining (EINVAL is returned by mount), which is -// useful for undoing a stack of mounts on the same mount point. -// UnmountAll all is noop when the first argument is an empty string. -// This is done when the containerd client did not specify any rootfs -// mounts (e.g. because the rootfs is managed outside containerd) -// UnmountAll is noop when the mount path does not exist. -func UnmountAll(mount string, flags int) error { - if mount == "" { - return nil - } - if _, err := os.Stat(mount); os.IsNotExist(err) { - return nil - } - - for { - if err := unmount(mount, flags); err != nil { - // EINVAL is returned if the target is not a - // mount point, indicating that we are - // done. It can also indicate a few other - // things (such as invalid flags) which we - // unfortunately end up squelching here too. - if err == unix.EINVAL { - return nil - } - return err - } - } -} diff --git a/core/mount/mount_linux.go b/core/mount/mount_linux.go index d76b31ede393..3307603358a4 100644 --- a/core/mount/mount_linux.go +++ b/core/mount/mount_linux.go @@ -26,7 +26,6 @@ import ( "runtime" "strconv" "strings" - "time" "github.com/containerd/log" "github.com/moby/sys/userns" @@ -274,92 +273,6 @@ func doPrepareIDMappedOverlay(lowerDirs []string, usernsFd int) (tmpLowerDirs [] return tmpLowerDirs, cleanUp, nil } -// Unmount the provided mount path with the flags -func Unmount(target string, flags int) error { - if err := unmount(target, flags); err != nil && err != unix.EINVAL { - return err - } - return nil -} - -// fuseSuperMagic is defined in statfs(2) -const fuseSuperMagic = 0x65735546 - -func isFUSE(dir string) bool { - var st unix.Statfs_t - if err := unix.Statfs(dir, &st); err != nil { - return false - } - return st.Type == fuseSuperMagic -} - -// unmountFUSE attempts to unmount using fusermount/fusermount3 helper binary. -// -// For FUSE mounts, using these helper binaries is preferred, see: -// https://github.com/containerd/containerd/pull/3765#discussion_r342083514 -func unmountFUSE(target string) error { - var err error - for _, helperBinary := range []string{"fusermount3", "fusermount"} { - cmd := exec.Command(helperBinary, "-u", target) - err = cmd.Run() - if err == nil { - return nil - } - } - return err -} - -func unmount(target string, flags int) error { - if isFUSE(target) { - if err := unmountFUSE(target); err == nil { - return nil - } - } - for i := 0; i < 50; i++ { - if err := unix.Unmount(target, flags); err != nil { - switch err { - case unix.EBUSY: - time.Sleep(50 * time.Millisecond) - continue - default: - return err - } - } - return nil - } - return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY) -} - -// UnmountAll repeatedly unmounts the given mount point until there -// are no mounts remaining (EINVAL is returned by mount), which is -// useful for undoing a stack of mounts on the same mount point. -// UnmountAll all is noop when the first argument is an empty string. -// This is done when the containerd client did not specify any rootfs -// mounts (e.g. because the rootfs is managed outside containerd) -// UnmountAll is noop when the mount path does not exist. -func UnmountAll(mount string, flags int) error { - if mount == "" { - return nil - } - if _, err := os.Stat(mount); os.IsNotExist(err) { - return nil - } - - for { - if err := unmount(mount, flags); err != nil { - // EINVAL is returned if the target is not a - // mount point, indicating that we are - // done. It can also indicate a few other - // things (such as invalid flags) which we - // unfortunately end up squelching here too. - if err == unix.EINVAL { - return nil - } - return err - } - } -} - // parseMountOptions takes fstab style mount options and parses them for // use with a standard mount() syscall func parseMountOptions(options []string) (opt mountOpt) { diff --git a/core/mount/mount_unix.go b/core/mount/mount_unix.go index 49a17152b4ca..848464010df2 100644 --- a/core/mount/mount_unix.go +++ b/core/mount/mount_unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !darwin && !openbsd +//go:build !windows && !openbsd /* Copyright The containerd Authors. @@ -19,10 +19,13 @@ package mount import ( + "fmt" "os" "sort" + "time" "github.com/moby/sys/mountinfo" + "golang.org/x/sys/unix" ) // UnmountRecursive unmounts the target and all mounts underneath, starting @@ -69,3 +72,64 @@ func UnmountRecursive(target string, flags int) error { } return nil } + +func unmount(target string, flags int) error { + if isFUSE(target) { + // TODO: Why error is ignored? + // Shouldn't this just be unconditional "return unmountFUSE(target)"? + if err := unmountFUSE(target); err == nil { + return nil + } + } + for i := 0; i < 50; i++ { + if err := unix.Unmount(target, flags); err != nil { + switch err { + case unix.EBUSY: + time.Sleep(50 * time.Millisecond) + continue + default: + return err + } + } + return nil + } + return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY) +} + +// Unmount the provided mount path with the flags +func Unmount(target string, flags int) error { + if err := unmount(target, flags); err != nil && err != unix.EINVAL { + return err + } + return nil +} + +// UnmountAll repeatedly unmounts the given mount point until there +// are no mounts remaining (EINVAL is returned by mount), which is +// useful for undoing a stack of mounts on the same mount point. +// UnmountAll all is noop when the first argument is an empty string. +// This is done when the containerd client did not specify any rootfs +// mounts (e.g. because the rootfs is managed outside containerd) +// UnmountAll is noop when the mount path does not exist. +func UnmountAll(mount string, flags int) error { + if mount == "" { + return nil + } + if _, err := os.Stat(mount); os.IsNotExist(err) { + return nil + } + + for { + if err := unmount(mount, flags); err != nil { + // EINVAL is returned if the target is not a + // mount point, indicating that we are + // done. It can also indicate a few other + // things (such as invalid flags) which we + // unfortunately end up squelching here too. + if err == unix.EINVAL { + return nil + } + return err + } + } +} diff --git a/core/mount/mount_unsupported.go b/core/mount/mount_unsupported.go index 894467a9933e..af9e6b3370f7 100644 --- a/core/mount/mount_unsupported.go +++ b/core/mount/mount_unsupported.go @@ -1,4 +1,4 @@ -//go:build darwin || openbsd +//go:build openbsd /* Copyright The containerd Authors. @@ -18,29 +18,24 @@ package mount -import "errors" - -var ( - // ErrNotImplementOnUnix is returned for methods that are not implemented - ErrNotImplementOnUnix = errors.New("not implemented under unix") -) +import "github.com/containerd/errdefs" // Mount is not implemented on this platform func (m *Mount) mount(target string) error { - return ErrNotImplementOnUnix + return errdefs.ErrNotImplemented } // Unmount is not implemented on this platform func Unmount(mount string, flags int) error { - return ErrNotImplementOnUnix + return errdefs.ErrNotImplemented } // UnmountAll is not implemented on this platform func UnmountAll(mount string, flags int) error { - return ErrNotImplementOnUnix + return errdefs.ErrNotImplemented } // UnmountRecursive is not implemented on this platform func UnmountRecursive(mount string, flags int) error { - return ErrNotImplementOnUnix + return errdefs.ErrNotImplemented } diff --git a/core/mount/mount_windows.go b/core/mount/mount_windows.go index 91ac6968d2f7..925cde0a0b77 100644 --- a/core/mount/mount_windows.go +++ b/core/mount/mount_windows.go @@ -33,11 +33,6 @@ import ( const sourceStreamName = "containerd.io-source" -var ( - // ErrNotImplementOnWindows is returned when an action is not implemented for windows - ErrNotImplementOnWindows = errors.New("not implemented under windows") -) - // Mount to the provided target. func (m *Mount) mount(target string) (retErr error) { if m.Type != "windows-layer" { diff --git a/core/runtime/v2/bundle.go b/core/runtime/v2/bundle.go index 6e34a95723c3..e82679bce5e9 100644 --- a/core/runtime/v2/bundle.go +++ b/core/runtime/v2/bundle.go @@ -21,7 +21,6 @@ import ( "fmt" "os" "path/filepath" - "runtime" "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/containerd/v2/pkg/identifiers" @@ -129,10 +128,8 @@ type Bundle struct { func (b *Bundle) Delete() error { work, werr := os.Readlink(filepath.Join(b.Path, "work")) rootfs := filepath.Join(b.Path, "rootfs") - if runtime.GOOS != "darwin" { - if err := mount.UnmountRecursive(rootfs, 0); err != nil { - return fmt.Errorf("unmount rootfs %s: %w", rootfs, err) - } + if err := mount.UnmountRecursive(rootfs, 0); err != nil { + return fmt.Errorf("unmount rootfs %s: %w", rootfs, err) } if err := os.Remove(rootfs); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove bundle rootfs: %w", err) diff --git a/pkg/os/mount_unix.go b/pkg/os/mount_unix.go index 3df46bebe8bf..712d8bd27b8c 100644 --- a/pkg/os/mount_unix.go +++ b/pkg/os/mount_unix.go @@ -20,11 +20,12 @@ package os import ( "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/errdefs" ) // Mount will call unix.Mount to mount the file. func (RealOS) Mount(source string, target string, fstype string, flags uintptr, data string) error { - return mount.ErrNotImplementOnUnix + return errdefs.ErrNotImplemented } // Unmount will call Unmount to unmount the file.