diff --git a/diff/apply/apply_darwin.go b/diff/apply/apply_darwin.go new file mode 100644 index 000000000..dd93d3107 --- /dev/null +++ b/diff/apply/apply_darwin.go @@ -0,0 +1,47 @@ +/* + 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/archive" + "github.com/containerd/containerd/mount" +) + +func apply(ctx context.Context, mounts []mount.Mount, r io.Reader) 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 + } + + return mount.WithTempMount(ctx, mounts, func(root string) error { + _, err := archive.Apply(ctx, root, r) + return err + }) +} diff --git a/diff/apply/apply_other.go b/diff/apply/apply_other.go index 9daa6cc1f..3bd79781d 100644 --- a/diff/apply/apply_other.go +++ b/diff/apply/apply_other.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !linux && !darwin /* Copyright The containerd Authors. diff --git a/mount/fuse_linux.go b/mount/fuse_linux.go deleted file mode 100644 index b3a32b682..000000000 --- a/mount/fuse_linux.go +++ /dev/null @@ -1,50 +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 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/mount/fuse_unsupported.go b/mount/fuse_unsupported.go deleted file mode 100644 index 04de88c2c..000000000 --- a/mount/fuse_unsupported.go +++ /dev/null @@ -1,30 +0,0 @@ -//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/mount/mount_darwin.go b/mount/mount_darwin.go deleted file mode 100644 index 004a9abbb..000000000 --- a/mount/mount_darwin.go +++ /dev/null @@ -1,42 +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 mount - -import ( - "fmt" - "os/exec" -) - -// Mount to the provided target. -func (m *Mount) mount(target string) error { - // See https://github.com/slonopotamus/containerd-darwin-mount-helper for reference implementation - const commandName = "containerd-darwin-mount-helper" - - args := []string{"-t", m.Type} - for _, option := range m.Options { - args = append(args, "-o", option) - } - args = append(args, m.Source, target) - - cmd := exec.Command(commandName, args...) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("%s [%v] failed: %q: %w", commandName, args, string(output), err) - } - - return nil -} diff --git a/mount/mount_freebsd.go b/mount/mount_freebsd.go index 8dc04d5d6..6f3e8ff91 100644 --- a/mount/mount_freebsd.go +++ b/mount/mount_freebsd.go @@ -19,11 +19,18 @@ package mount import ( "errors" "fmt" - "os/exec" + "os" + "time" + exec "golang.org/x/sys/execabs" "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 @@ -70,3 +77,57 @@ 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/mount/mount_linux.go b/mount/mount_linux.go index 2fa326a10..90dd941a9 100644 --- a/mount/mount_linux.go +++ b/mount/mount_linux.go @@ -23,6 +23,7 @@ import ( "path" "runtime" "strings" + "time" exec "golang.org/x/sys/execabs" "golang.org/x/sys/unix" @@ -119,6 +120,92 @@ func (m *Mount) mount(target string) (err error) { return 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) (int, []string, bool) { diff --git a/mount/mount_unix.go b/mount/mount_unix.go index c374e4e4b..46dfd1c34 100644 --- a/mount/mount_unix.go +++ b/mount/mount_unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !openbsd +//go:build !windows && !darwin && !openbsd /* Copyright The containerd Authors. @@ -19,13 +19,9 @@ 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 @@ -63,64 +59,3 @@ 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 "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/mount/mount_unsupported.go b/mount/mount_unsupported.go index 80f5ad1e7..894467a99 100644 --- a/mount/mount_unsupported.go +++ b/mount/mount_unsupported.go @@ -1,4 +1,4 @@ -//go:build openbsd +//go:build darwin || openbsd /* Copyright The containerd Authors. @@ -18,26 +18,29 @@ package mount -import ( - "github.com/containerd/containerd/errdefs" +import "errors" + +var ( + // ErrNotImplementOnUnix is returned for methods that are not implemented + ErrNotImplementOnUnix = errors.New("not implemented under unix") ) // Mount is not implemented on this platform func (m *Mount) mount(target string) error { - return errdefs.ErrNotImplemented + return ErrNotImplementOnUnix } // Unmount is not implemented on this platform func Unmount(mount string, flags int) error { - return errdefs.ErrNotImplemented + return ErrNotImplementOnUnix } // UnmountAll is not implemented on this platform func UnmountAll(mount string, flags int) error { - return errdefs.ErrNotImplemented + return ErrNotImplementOnUnix } // UnmountRecursive is not implemented on this platform func UnmountRecursive(mount string, flags int) error { - return errdefs.ErrNotImplemented + return ErrNotImplementOnUnix } diff --git a/mount/mount_windows.go b/mount/mount_windows.go index 593b47b84..7c24fa600 100644 --- a/mount/mount_windows.go +++ b/mount/mount_windows.go @@ -33,6 +33,11 @@ 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/pkg/os/mount_unix.go b/pkg/os/mount_unix.go index e0e08d66b..bc3423797 100644 --- a/pkg/os/mount_unix.go +++ b/pkg/os/mount_unix.go @@ -19,13 +19,12 @@ package os import ( - "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/mount" ) // Mount will call unix.Mount to mount the file. func (RealOS) Mount(source string, target string, fstype string, flags uintptr, data string) error { - return errdefs.ErrNotImplemented + return mount.ErrNotImplementOnUnix } // Unmount will call Unmount to unmount the file. diff --git a/runtime/v2/bundle.go b/runtime/v2/bundle.go index 01781b232..e7b5800c8 100644 --- a/runtime/v2/bundle.go +++ b/runtime/v2/bundle.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "github.com/containerd/containerd/identifiers" "github.com/containerd/containerd/mount" @@ -128,8 +129,10 @@ type Bundle struct { func (b *Bundle) Delete() error { work, werr := os.Readlink(filepath.Join(b.Path, "work")) rootfs := filepath.Join(b.Path, "rootfs") - if err := mount.UnmountRecursive(rootfs, 0); err != nil { - return fmt.Errorf("unmount rootfs %s: %w", rootfs, err) + if runtime.GOOS != "darwin" { + 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)