415 lines
13 KiB
Go
415 lines
13 KiB
Go
//go:build !windows
|
|
// +build !windows
|
|
|
|
package snapshot
|
|
|
|
import (
|
|
"context"
|
|
gofs "io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/containerd/containerd/leases"
|
|
"github.com/containerd/containerd/mount"
|
|
"github.com/containerd/containerd/snapshots"
|
|
"github.com/containerd/continuity/fs"
|
|
"github.com/containerd/continuity/sysx"
|
|
"github.com/containerd/stargz-snapshotter/snapshot/overlayutils"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/util/bklog"
|
|
"github.com/moby/buildkit/util/leaseutil"
|
|
"github.com/moby/buildkit/util/overlay"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// diffApply calculates the diff between two directories and directly applies the changes to a separate mount (without using
|
|
// the content store as an intermediary). If useHardlink is set to true, it will hardlink non-directories instead of copying
|
|
// them when applying. This obviously requires that each of the mounts provided are for immutable, committed snapshots.
|
|
// externalHardlinks tracks any such hardlinks, which is needed for doing correct disk usage calculations elsewhere.
|
|
func diffApply(ctx context.Context, lowerMountable, upperMountable, applyMountable Mountable, useHardlink bool, externalHardlinks map[uint64]struct{}, createWhiteoutDeletes bool) error {
|
|
if applyMountable == nil {
|
|
return errors.New("invalid nil apply mounts")
|
|
}
|
|
|
|
var lowerMounts []mount.Mount
|
|
if lowerMountable != nil {
|
|
mounts, unmountLower, err := lowerMountable.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lowerMounts = mounts
|
|
defer unmountLower()
|
|
}
|
|
|
|
var upperMounts []mount.Mount
|
|
if upperMountable != nil {
|
|
mounts, unmountUpper, err := upperMountable.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
upperMounts = mounts
|
|
defer unmountUpper()
|
|
}
|
|
|
|
lowerMnter := LocalMounterWithMounts(lowerMounts)
|
|
lowerView, err := lowerMnter.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lowerMnter.Unmount()
|
|
|
|
upperMnter := LocalMounterWithMounts(upperMounts)
|
|
upperView, err := upperMnter.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer upperMnter.Unmount()
|
|
|
|
applyMounts, unmountApply, err := applyMountable.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unmountApply()
|
|
|
|
var applyRoot string
|
|
if useHardlink {
|
|
applyRoot, err = getRWDir(applyMounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
applyMnter := LocalMounterWithMounts(applyMounts)
|
|
applyRoot, err = applyMnter.Mount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer applyMnter.Unmount()
|
|
}
|
|
type pathTime struct {
|
|
applyPath string
|
|
atime unix.Timespec
|
|
mtime unix.Timespec
|
|
}
|
|
|
|
// times holds the paths+times we visited and need to set
|
|
var times []pathTime
|
|
|
|
visited := make(map[string]struct{})
|
|
inodes := make(map[uint64]string)
|
|
|
|
diffCalculator := func(cf fs.ChangeFunc) error {
|
|
return fs.Changes(ctx, lowerView, upperView, cf)
|
|
}
|
|
upperRoot := upperView
|
|
|
|
// If using hardlinks, set upperRoot to the underlying filesystem so hardlinks can avoid EXDEV
|
|
if useHardlink && len(upperMounts) == 1 {
|
|
switch upperMounts[0].Type {
|
|
case "bind", "rbind":
|
|
upperRoot = upperMounts[0].Source
|
|
case "overlay":
|
|
if upperdir, err := overlay.GetUpperdir(lowerMounts, upperMounts); err == nil {
|
|
upperRoot = upperdir
|
|
diffCalculator = func(cf fs.ChangeFunc) error {
|
|
return overlay.Changes(ctx, cf, upperRoot, upperView, lowerView)
|
|
}
|
|
} else {
|
|
useHardlink = false
|
|
}
|
|
}
|
|
}
|
|
|
|
var changeFunc fs.ChangeFunc
|
|
changeFunc = func(kind fs.ChangeKind, changePath string, upperFi os.FileInfo, prevErr error) error {
|
|
if prevErr != nil {
|
|
return prevErr
|
|
}
|
|
|
|
applyPath, err := safeJoin(applyRoot, changePath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get apply path for root %q, change %q", applyRoot, changePath)
|
|
}
|
|
upperPath, err := safeJoin(upperRoot, changePath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get upper path for root %q, change %q", upperPath, changePath)
|
|
}
|
|
|
|
visited[upperPath] = struct{}{}
|
|
|
|
if kind == fs.ChangeKindUnmodified {
|
|
return nil
|
|
}
|
|
|
|
// When using overlay, a delete is represented with a whiteout device, which we actually want to
|
|
// hardlink or recreate here. If upperFi is non-nil, that means we have a whiteout device we can
|
|
// hardlink in. If upperFi is nil though, we need to first remove what's at the path and then, in
|
|
// the overlay case, create a whiteout device to represent the delete. This can happen when the
|
|
// overlay differ encounters an opaque dir, in which case it switches to the walking differ in order
|
|
// to convert from the opaque representation to the "explicit whiteout" format.
|
|
if kind == fs.ChangeKindDelete && upperFi == nil {
|
|
if err := os.RemoveAll(applyPath); err != nil {
|
|
return errors.Wrapf(err, "failed to remove path %s during apply", applyPath)
|
|
}
|
|
// Create the whiteout device only if enabled and if we are directly applying to an upperdir (as
|
|
// opposed to applying to an overlay mount itself).
|
|
if createWhiteoutDeletes && upperRoot != upperView {
|
|
if err := unix.Mknod(applyPath, unix.S_IFCHR, int(unix.Mkdev(0, 0))); err != nil {
|
|
return errors.Wrap(err, "failed to create whiteout delete")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
upperType := upperFi.Mode().Type()
|
|
if upperType == os.ModeIrregular {
|
|
return errors.Errorf("unhandled irregular mode file during merge at path %q", changePath)
|
|
}
|
|
|
|
upperStat, ok := upperFi.Sys().(*syscall.Stat_t)
|
|
if !ok {
|
|
return errors.Errorf("unhandled stat type for %+v", upperFi)
|
|
}
|
|
|
|
// Check to see if the parent dir needs to be created. This is needed when we are visiting a dirent
|
|
// that changes but never visit its parent dir because the parent did not change in the diff
|
|
changeParent := filepath.Dir(changePath)
|
|
if changeParent != "/" {
|
|
upperParent := filepath.Dir(upperPath)
|
|
if _, ok := visited[upperParent]; !ok {
|
|
parentInfo, err := os.Lstat(upperParent)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to stat parent path during apply")
|
|
}
|
|
if err := changeFunc(fs.ChangeKindAdd, changeParent, parentInfo, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
applyFi, err := os.Lstat(applyPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "failed to stat path during apply")
|
|
}
|
|
|
|
// if there is an existing file/dir at the applyPath, delete it unless both it and upper are dirs (in which case they get merged)
|
|
if applyFi != nil && !(applyFi.IsDir() && upperFi.IsDir()) {
|
|
if err := os.RemoveAll(applyPath); err != nil {
|
|
return errors.Wrapf(err, "failed to remove path %s during apply", applyPath)
|
|
}
|
|
applyFi = nil
|
|
}
|
|
|
|
// hardlink fast-path
|
|
if useHardlink {
|
|
switch upperType {
|
|
case os.ModeDir, os.ModeNamedPipe, os.ModeSocket:
|
|
// Directories can't be hard-linked, so they just have to be recreated.
|
|
// Named pipes and sockets can be hard-linked but is best to avoid as it could enable IPC by sharing merge inputs.
|
|
break
|
|
default:
|
|
// TODO:(sipsma) consider handling EMLINK by falling back to copy
|
|
if err := os.Link(upperPath, applyPath); err != nil {
|
|
return errors.Wrapf(err, "failed to hardlink %q to %q during apply", upperPath, applyPath)
|
|
}
|
|
// mark this inode as one coming from a separate snapshot, needed for disk usage calculations elsewhere
|
|
externalHardlinks[upperStat.Ino] = struct{}{}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
switch upperType {
|
|
case 0: // regular file
|
|
if upperStat.Nlink > 1 {
|
|
if linkedPath, ok := inodes[upperStat.Ino]; ok {
|
|
if err := os.Link(linkedPath, applyPath); err != nil {
|
|
return errors.Wrap(err, "failed to create hardlink during apply")
|
|
}
|
|
return nil // no other metadata updates needed when hardlinking
|
|
}
|
|
inodes[upperStat.Ino] = applyPath
|
|
}
|
|
if err := fs.CopyFile(applyPath, upperPath); err != nil {
|
|
return errors.Wrapf(err, "failed to copy from %s to %s during apply", upperPath, applyPath)
|
|
}
|
|
case os.ModeDir:
|
|
if applyFi == nil {
|
|
// applyPath doesn't exist, make it a dir
|
|
if err := os.Mkdir(applyPath, upperFi.Mode()); err != nil {
|
|
return errors.Wrap(err, "failed to create applied dir")
|
|
}
|
|
}
|
|
case os.ModeSymlink:
|
|
if target, err := os.Readlink(upperPath); err != nil {
|
|
return errors.Wrap(err, "failed to read symlink during apply")
|
|
} else if err := os.Symlink(target, applyPath); err != nil {
|
|
return errors.Wrap(err, "failed to create symlink during apply")
|
|
}
|
|
case os.ModeCharDevice, os.ModeDevice, os.ModeNamedPipe, os.ModeSocket:
|
|
if err := unix.Mknod(applyPath, uint32(upperFi.Mode()), int(upperStat.Rdev)); err != nil {
|
|
return errors.Wrap(err, "failed to create device during apply")
|
|
}
|
|
default:
|
|
// should never be here, all types should be handled
|
|
return errors.Errorf("unhandled file type %q during merge at path %q", upperType.String(), changePath)
|
|
}
|
|
|
|
xattrs, err := sysx.LListxattr(upperPath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to list xattrs of upper path %s", upperPath)
|
|
}
|
|
for _, xattr := range xattrs {
|
|
if isOpaqueXattr(xattr) {
|
|
// Don't recreate opaque xattrs during merge. These should only be set when using overlay snapshotters,
|
|
// in which case we are converting from the "opaque whiteout" format to the "explicit whiteout" format during
|
|
// the merge (as taken care of by the overlay differ).
|
|
continue
|
|
}
|
|
xattrVal, err := sysx.LGetxattr(upperPath, xattr)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get xattr %s of upper path %s", xattr, upperPath)
|
|
}
|
|
if err := sysx.LSetxattr(applyPath, xattr, xattrVal, 0); err != nil {
|
|
// This can often fail, so just log it: https://github.com/moby/buildkit/issues/1189
|
|
bklog.G(ctx).Debugf("failed to set xattr %s of path %s during apply", xattr, applyPath)
|
|
}
|
|
}
|
|
|
|
if err := os.Lchown(applyPath, int(upperStat.Uid), int(upperStat.Gid)); err != nil {
|
|
return errors.Wrap(err, "failed to chown applied dir")
|
|
}
|
|
|
|
if upperType != os.ModeSymlink {
|
|
if err := os.Chmod(applyPath, upperFi.Mode()); err != nil {
|
|
return errors.Wrap(err, "failed to chmod applied dir")
|
|
}
|
|
}
|
|
|
|
// save the times we should set on this path, to be applied at the end.
|
|
times = append(times, pathTime{
|
|
applyPath: applyPath,
|
|
atime: unix.Timespec{
|
|
Sec: upperStat.Atim.Sec,
|
|
Nsec: upperStat.Atim.Nsec,
|
|
},
|
|
mtime: unix.Timespec{
|
|
Sec: upperStat.Mtim.Sec,
|
|
Nsec: upperStat.Mtim.Nsec,
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
if err := diffCalculator(changeFunc); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set times now that everything has been modified.
|
|
for i := range times {
|
|
ts := times[len(times)-1-i]
|
|
if err := unix.UtimesNanoAt(unix.AT_FDCWD, ts.applyPath, []unix.Timespec{ts.atime, ts.mtime}, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
return errors.Wrapf(err, "failed to update times of path %q", ts.applyPath)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func safeJoin(root, path string) (string, error) {
|
|
dir, base := filepath.Split(path)
|
|
parent, err := fs.RootPath(root, dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(parent, base), nil
|
|
}
|
|
|
|
// diskUsage calculates the disk space used by the provided mounts, similar to the normal containerd snapshotter disk usage
|
|
// calculations but with the extra ability to take into account hardlinks that were created between snapshots, ensuring that
|
|
// they don't get double counted.
|
|
func diskUsage(ctx context.Context, mountable Mountable, externalHardlinks map[uint64]struct{}) (snapshots.Usage, error) {
|
|
mounts, unmount, err := mountable.Mount()
|
|
if err != nil {
|
|
return snapshots.Usage{}, err
|
|
}
|
|
defer unmount()
|
|
|
|
inodes := make(map[uint64]struct{})
|
|
var usage snapshots.Usage
|
|
root, err := getRWDir(mounts)
|
|
if err != nil {
|
|
return snapshots.Usage{}, err
|
|
}
|
|
if err := filepath.WalkDir(root, func(path string, dirent gofs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := dirent.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stat := info.Sys().(*syscall.Stat_t)
|
|
if _, ok := inodes[stat.Ino]; ok {
|
|
return nil
|
|
}
|
|
inodes[stat.Ino] = struct{}{}
|
|
if _, ok := externalHardlinks[stat.Ino]; !ok {
|
|
usage.Inodes++
|
|
usage.Size += stat.Blocks * 512 // 512 is always block size, see "man 2 stat"
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return snapshots.Usage{}, err
|
|
}
|
|
return usage, nil
|
|
}
|
|
|
|
func isOpaqueXattr(s string) bool {
|
|
for _, k := range []string{"trusted.overlay.opaque", "user.overlay.opaque"} {
|
|
if s == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// needsUserXAttr checks whether overlay mounts should be provided the userxattr option. We can't use
|
|
// NeedsUserXAttr from the overlayutils package directly because we don't always have direct knowledge
|
|
// of the root of the snapshotter state (such as when using a remote snapshotter). Instead, we create
|
|
// a temporary new snapshot and test using its root, which works because single layer snapshots will
|
|
// use bind-mounts even when created by an overlay based snapshotter.
|
|
func needsUserXAttr(ctx context.Context, sn Snapshotter, lm leases.Manager) (bool, error) {
|
|
key := identity.NewID()
|
|
|
|
ctx, done, err := leaseutil.WithLease(ctx, lm, leaseutil.MakeTemporary)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "failed to create lease for checking user xattr")
|
|
}
|
|
defer done(context.TODO())
|
|
|
|
err = sn.Prepare(ctx, key, "")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
mntable, err := sn.Mounts(ctx, key)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
mnts, unmount, err := mntable.Mount()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer unmount()
|
|
|
|
var userxattr bool
|
|
if err := mount.WithTempMount(ctx, mnts, func(root string) error {
|
|
var err error
|
|
userxattr, err = overlayutils.NeedsUserXAttr(root)
|
|
return err
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
return userxattr, nil
|
|
}
|