diff --git a/archive/tar.go b/archive/tar.go index fae023c55..3a1e77a7f 100644 --- a/archive/tar.go +++ b/archive/tar.go @@ -19,9 +19,7 @@ package archive import ( "archive/tar" "context" - "fmt" "io" - "io/ioutil" "os" "path/filepath" "runtime" @@ -91,11 +89,6 @@ const ( // archives. whiteoutMetaPrefix = whiteoutPrefix + whiteoutPrefix - // whiteoutLinkDir is a directory AUFS uses for storing hardlink links to other - // layers. Normally these should not go into exported archives and all changed - // hardlinks should be copied to the top layer. - whiteoutLinkDir = whiteoutMetaPrefix + "plnk" - // whiteoutOpaqueDir file means directory has been made opaque - meaning // readdir calls to this directory do not follow to lower layers. whiteoutOpaqueDir = whiteoutMetaPrefix + ".opq" @@ -117,11 +110,15 @@ func Apply(ctx context.Context, root string, r io.Reader, opts ...ApplyOpt) (int if options.Filter == nil { options.Filter = all } + if options.applyFunc == nil { + options.applyFunc = applyNaive + } - return apply(ctx, root, tar.NewReader(r), options) + return options.applyFunc(ctx, root, tar.NewReader(r), options) } -// applyNaive applies a tar stream of an OCI style diff tar. +// applyNaive applies a tar stream of an OCI style diff tar to a directory +// applying each file as either a whole file or whiteout. // See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { var ( @@ -131,11 +128,49 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO // may occur out of order unpackedPaths = make(map[string]struct{}) - // Used for aufs plink directory - aufsTempdir = "" - aufsHardlinks = make(map[string]*tar.Header) + convertWhiteout = options.ConvertWhiteout ) + if convertWhiteout == nil { + // handle whiteouts by removing the target files + convertWhiteout = func(hdr *tar.Header, path string) (bool, error) { + base := filepath.Base(path) + dir := filepath.Dir(path) + if base == whiteoutOpaqueDir { + _, err := os.Lstat(dir) + if err != nil { + return false, err + } + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + if os.IsNotExist(err) { + err = nil // parent was deleted + } + return err + } + if path == dir { + return nil + } + if _, exists := unpackedPaths[path]; !exists { + err := os.RemoveAll(path) + return err + } + return nil + }) + return false, err + } + + if strings.HasPrefix(base, whiteoutPrefix) { + originalBase := base[len(whiteoutPrefix):] + originalPath := filepath.Join(dir, originalBase) + + return false, os.RemoveAll(originalPath) + } + + return true, nil + } + } + // Iterate through the files in the archive. for { select { @@ -193,85 +228,21 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO if base == "" { parentPath = filepath.Dir(path) } - if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { - err = mkdirAll(parentPath, 0755) - if err != nil { - return 0, err - } - } - } - - // Skip AUFS metadata dirs - if strings.HasPrefix(hdr.Name, whiteoutMetaPrefix) { - // Regular files inside /.wh..wh.plnk can be used as hardlink targets - // We don't want this directory, but we need the files in them so that - // such hardlinks can be resolved. - if strings.HasPrefix(hdr.Name, whiteoutLinkDir) && hdr.Typeflag == tar.TypeReg { - basename := filepath.Base(hdr.Name) - aufsHardlinks[basename] = hdr - if aufsTempdir == "" { - if aufsTempdir, err = ioutil.TempDir(os.Getenv("XDG_RUNTIME_DIR"), "dockerplnk"); err != nil { - return 0, err - } - defer os.RemoveAll(aufsTempdir) - } - p, err := fs.RootPath(aufsTempdir, basename) - if err != nil { - return 0, err - } - if err := createTarFile(ctx, p, root, hdr, tr); err != nil { - return 0, err - } - } - - if hdr.Name != whiteoutOpaqueDir { - continue - } - } - - if strings.HasPrefix(base, whiteoutPrefix) { - dir := filepath.Dir(path) - if base == whiteoutOpaqueDir { - _, err := os.Lstat(dir) - if err != nil { - return 0, err - } - err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - if os.IsNotExist(err) { - err = nil // parent was deleted - } - return err - } - if path == dir { - return nil - } - if _, exists := unpackedPaths[path]; !exists { - err := os.RemoveAll(path) - return err - } - return nil - }) - if err != nil { - return 0, err - } - continue - } - - originalBase := base[len(whiteoutPrefix):] - originalPath := filepath.Join(dir, originalBase) - - // Ensure originalPath is under dir - if dir[len(dir)-1] != filepath.Separator { - dir += string(filepath.Separator) - } - if !strings.HasPrefix(originalPath, dir) { - return 0, errors.Wrapf(errInvalidArchive, "invalid whiteout name: %v", base) - } - - if err := os.RemoveAll(originalPath); err != nil { + if err := mkparent(ctx, parentPath, root, options.Parents); err != nil { return 0, err } + } + + // Naive whiteout convert function which handles whiteout files by + // removing the target files. + if err := validateWhiteout(path); err != nil { + return 0, err + } + writeFile, err := convertWhiteout(hdr, path) + if err != nil { + return 0, errors.Wrapf(err, "failed to convert whiteout file %q", hdr.Name) + } + if !writeFile { continue } // If path exits we almost always just want to remove and replace it. @@ -289,26 +260,6 @@ func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyO srcData := io.Reader(tr) srcHdr := hdr - // Hard links into /.wh..wh.plnk don't work, as we don't extract that directory, so - // we manually retarget these into the temporary files we extracted them into - if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(filepath.Clean(hdr.Linkname), whiteoutLinkDir) { - linkBasename := filepath.Base(hdr.Linkname) - srcHdr = aufsHardlinks[linkBasename] - if srcHdr == nil { - return 0, fmt.Errorf("invalid aufs hardlink") - } - p, err := fs.RootPath(aufsTempdir, linkBasename) - if err != nil { - return 0, err - } - tmpFile, err := os.Open(p) - if err != nil { - return 0, err - } - defer tmpFile.Close() - srcData = tmpFile - } - if err := createTarFile(ctx, path, root, srcHdr, srcData); err != nil { return 0, err } @@ -428,6 +379,66 @@ func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header return chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)) } +func mkparent(ctx context.Context, path, root string, parents []string) error { + if dir, err := os.Lstat(path); err == nil { + if dir.IsDir() { + return nil + } + return &os.PathError{ + Op: "mkparent", + Path: path, + Err: syscall.ENOTDIR, + } + } else if !os.IsNotExist(err) { + return err + } + + i := len(path) + for i > len(root) && !os.IsPathSeparator(path[i-1]) { + i-- + } + + if i > len(root)+1 { + if err := mkparent(ctx, path[:i-1], root, parents); err != nil { + return err + } + } + + if err := mkdir(path, 0755); err != nil { + // Check that still doesn't exist + dir, err1 := os.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + + for _, p := range parents { + ppath, err := fs.RootPath(p, path[len(root):]) + if err != nil { + return err + } + + dir, err := os.Lstat(ppath) + if err == nil { + if !dir.IsDir() { + // Replaced, do not copy attributes + break + } + if err := copyDirInfo(dir, path); err != nil { + return err + } + return copyUpXAttrs(path, ppath) + } else if !os.IsNotExist(err) { + return err + } + } + + log.G(ctx).Debugf("parent directory %q not found: default permissions(0755) used", path) + + return nil +} + type changeWriter struct { tw *tar.Writer source string @@ -598,6 +609,9 @@ func (cw *changeWriter) Close() error { } func (cw *changeWriter) includeParents(hdr *tar.Header) error { + if cw.addedDirs == nil { + return nil + } name := strings.TrimRight(hdr.Name, "/") fname := filepath.Join(cw.source, name) parent := filepath.Dir(name) @@ -684,3 +698,26 @@ func hardlinkRootPath(root, linkname string) (string, error) { } return targetPath, nil } + +func validateWhiteout(path string) error { + base := filepath.Base(path) + dir := filepath.Dir(path) + + if base == whiteoutOpaqueDir { + return nil + } + + if strings.HasPrefix(base, whiteoutPrefix) { + originalBase := base[len(whiteoutPrefix):] + originalPath := filepath.Join(dir, originalBase) + + // Ensure originalPath is under dir + if dir[len(dir)-1] != filepath.Separator { + dir += string(filepath.Separator) + } + if !strings.HasPrefix(originalPath, dir) { + return errors.Wrapf(errInvalidArchive, "invalid whiteout name: %v", base) + } + } + return nil +} diff --git a/archive/tar_linux_test.go b/archive/tar_linux_test.go new file mode 100644 index 000000000..2a7070155 --- /dev/null +++ b/archive/tar_linux_test.go @@ -0,0 +1,183 @@ +// +build linux + +/* + 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 archive + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/containerd/containerd/log/logtest" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/pkg/testutil" + "github.com/containerd/containerd/snapshots/overlay" + "github.com/containerd/continuity/fs" + "github.com/containerd/continuity/fs/fstest" + "github.com/pkg/errors" +) + +func TestOverlayApply(t *testing.T) { + testutil.RequiresRoot(t) + + base, err := ioutil.TempDir("", "test-ovl-diff-apply-") + if err != nil { + t.Fatalf("unable to create temp dir: %+v", err) + } + defer os.RemoveAll(base) + + if err := overlay.Supported(base); err != nil { + t.Skipf("skipping because overlay is not supported %v", err) + } + fstest.FSSuite(t, overlayDiffApplier{ + tmp: base, + diff: WriteDiff, + t: t, + }) +} + +func TestOverlayApplyNoParents(t *testing.T) { + testutil.RequiresRoot(t) + + base, err := ioutil.TempDir("", "test-ovl-diff-apply-") + if err != nil { + t.Fatalf("unable to create temp dir: %+v", err) + } + defer os.RemoveAll(base) + + if err := overlay.Supported(base); err != nil { + t.Skipf("skipping because overlay is not supported %v", err) + } + fstest.FSSuite(t, overlayDiffApplier{ + tmp: base, + diff: func(ctx context.Context, w io.Writer, a, b string) error { + cw := newChangeWriter(w, b) + cw.addedDirs = nil + err := fs.Changes(ctx, a, b, cw.HandleChange) + if err != nil { + return errors.Wrap(err, "failed to create diff tar stream") + } + return cw.Close() + }, + t: t, + }) +} + +type overlayDiffApplier struct { + tmp string + diff func(context.Context, io.Writer, string, string) error + t *testing.T +} + +type overlayContext struct { + merged string + lowers []string + mounted bool +} + +type contextKey struct{} + +func (d overlayDiffApplier) TestContext(ctx context.Context) (context.Context, func(), error) { + merged, err := ioutil.TempDir(d.tmp, "merged") + if err != nil { + return ctx, nil, errors.Wrap(err, "failed to make merged dir") + } + + oc := &overlayContext{ + merged: merged, + } + + ctx = logtest.WithT(ctx, d.t) + + return context.WithValue(ctx, contextKey{}, oc), func() { + if oc.mounted { + mount.Unmount(oc.merged, 0) + } + }, nil +} + +func (d overlayDiffApplier) Apply(ctx context.Context, a fstest.Applier) (string, func(), error) { + oc := ctx.Value(contextKey{}).(*overlayContext) + + applyCopy, err := ioutil.TempDir(d.tmp, "apply-copy-") + if err != nil { + return "", nil, errors.Wrap(err, "failed to create temp dir") + } + defer os.RemoveAll(applyCopy) + + base := oc.merged + if len(oc.lowers) == 1 { + base = oc.lowers[0] + } + + if err = fs.CopyDir(applyCopy, base); err != nil { + return "", nil, errors.Wrap(err, "failed to copy base") + } + + if err := a.Apply(applyCopy); err != nil { + return "", nil, errors.Wrap(err, "failed to apply changes to copy of base") + } + + buf := bytes.NewBuffer(nil) + + if err := d.diff(ctx, buf, base, applyCopy); err != nil { + return "", nil, errors.Wrap(err, "failed to create diff") + } + + if oc.mounted { + if err := mount.Unmount(oc.merged, 0); err != nil { + return "", nil, errors.Wrap(err, "failed to unmount") + } + oc.mounted = false + } + + next, err := ioutil.TempDir(d.tmp, "lower-") + if err != nil { + return "", nil, errors.Wrap(err, "failed to create temp dir") + } + + if _, err = Apply(ctx, next, buf, WithConvertWhiteout(OverlayConvertWhiteout), WithParents(oc.lowers)); err != nil { + return "", nil, errors.Wrap(err, "failed to apply tar stream") + } + + oc.lowers = append([]string{next}, oc.lowers...) + + if len(oc.lowers) == 1 { + return oc.lowers[0], nil, nil + } + + m := mount.Mount{ + Type: "overlay", + Source: "overlay", + Options: []string{ + fmt.Sprintf("lowerdir=%s", strings.Join(oc.lowers, ":")), + }, + } + + if err := m.Mount(oc.merged); err != nil { + return "", nil, errors.Wrapf(err, "failed to mount: %v", m) + } + oc.mounted = true + + return oc.merged, nil, nil +} diff --git a/archive/tar_opts.go b/archive/tar_opts.go index a08bc102a..ca419e112 100644 --- a/archive/tar_opts.go +++ b/archive/tar_opts.go @@ -16,7 +16,19 @@ package archive -import "archive/tar" +import ( + "archive/tar" + "context" +) + +// ApplyOptions provides additional options for an Apply operation +type ApplyOptions struct { + Filter Filter // Filter tar headers + ConvertWhiteout ConvertWhiteout // Convert whiteout files + Parents []string // Parent directories to handle inherited attributes without CoW + + applyFunc func(context.Context, string, *tar.Reader, ApplyOptions) (int64, error) +} // ApplyOpt allows setting mutable archive apply properties on creation type ApplyOpt func(options *ApplyOptions) error @@ -24,6 +36,9 @@ type ApplyOpt func(options *ApplyOptions) error // Filter specific files from the archive type Filter func(*tar.Header) (bool, error) +// ConvertWhiteout converts whiteout files from the archive +type ConvertWhiteout func(*tar.Header, string) (bool, error) + // all allows all files func all(_ *tar.Header) (bool, error) { return true, nil @@ -36,3 +51,24 @@ func WithFilter(f Filter) ApplyOpt { return nil } } + +// WithConvertWhiteout uses the convert function to convert the whiteout files. +func WithConvertWhiteout(c ConvertWhiteout) ApplyOpt { + return func(options *ApplyOptions) error { + options.ConvertWhiteout = c + return nil + } +} + +// WithParents provides parent directories for resolving inherited attributes +// directory from the filesystem. +// Inherited attributes are searched from first to last, making the first +// element in the list the most immediate parent directory. +// NOTE: When applying to a filesystem which supports CoW, file attributes +// should be inherited by the filesystem. +func WithParents(p []string) ApplyOpt { + return func(options *ApplyOptions) error { + options.Parents = p + return nil + } +} diff --git a/archive/tar_opts_linux.go b/archive/tar_opts_linux.go new file mode 100644 index 000000000..38ef9e9bc --- /dev/null +++ b/archive/tar_opts_linux.go @@ -0,0 +1,59 @@ +// +build linux + +/* + 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 archive + +import ( + "archive/tar" + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/unix" +) + +// AufsConvertWhiteout converts whiteout files for aufs. +func AufsConvertWhiteout(_ *tar.Header, _ string) (bool, error) { + return true, nil +} + +// OverlayConvertWhiteout converts whiteout files for overlay. +func OverlayConvertWhiteout(hdr *tar.Header, path string) (bool, error) { + base := filepath.Base(path) + dir := filepath.Dir(path) + + // if a directory is marked as opaque, we need to translate that to overlay + if base == whiteoutOpaqueDir { + // don't write the file itself + return false, unix.Setxattr(dir, "trusted.overlay.opaque", []byte{'y'}, 0) + } + + // if a file was deleted and we are using overlay, we need to create a character device + if strings.HasPrefix(base, whiteoutPrefix) { + originalBase := base[len(whiteoutPrefix):] + originalPath := filepath.Join(dir, originalBase) + + if err := unix.Mknod(originalPath, unix.S_IFCHR, 0); err != nil { + return false, err + } + // don't write the file itself + return false, os.Chown(originalPath, hdr.Uid, hdr.Gid) + } + + return true, nil +} diff --git a/archive/tar_opts_windows.go b/archive/tar_opts_windows.go index e4b15a163..f472013bc 100644 --- a/archive/tar_opts_windows.go +++ b/archive/tar_opts_windows.go @@ -18,28 +18,12 @@ package archive -// ApplyOptions provides additional options for an Apply operation -type ApplyOptions struct { - ParentLayerPaths []string // Parent layer paths used for Windows layer apply - IsWindowsContainerLayer bool // True if the tar stream to be applied is a Windows Container Layer - Filter Filter // Filter tar headers -} - -// WithParentLayers adds parent layers to the apply process this is required -// for all Windows layers except the base layer. -func WithParentLayers(parentPaths []string) ApplyOpt { - return func(options *ApplyOptions) error { - options.ParentLayerPaths = parentPaths - return nil - } -} - // AsWindowsContainerLayer indicates that the tar stream to apply is that of // a Windows Container Layer. The caller must be holding SeBackupPrivilege and // SeRestorePrivilege. func AsWindowsContainerLayer() ApplyOpt { return func(options *ApplyOptions) error { - options.IsWindowsContainerLayer = true + options.applyFunc = applyWindowsLayer return nil } } diff --git a/archive/tar_unix.go b/archive/tar_unix.go index 022dd6d4f..e87218753 100644 --- a/archive/tar_unix.go +++ b/archive/tar_unix.go @@ -20,11 +20,12 @@ package archive import ( "archive/tar" - "context" "os" + "strings" "sync" "syscall" + "github.com/containerd/continuity/fs" "github.com/containerd/continuity/sysx" "github.com/opencontainers/runc/libcontainer/system" "github.com/pkg/errors" @@ -74,10 +75,6 @@ func openFile(name string, flag int, perm os.FileMode) (*os.File, error) { return f, err } -func mkdirAll(path string, perm os.FileMode) error { - return os.MkdirAll(path, perm) -} - func mkdir(path string, perm os.FileMode) error { if err := os.Mkdir(path, perm); err != nil { return err @@ -149,11 +146,71 @@ func getxattr(path, attr string) ([]byte, error) { } func setxattr(path, key, value string) error { - return sysx.LSetxattr(path, key, []byte(value), 0) + // Do not set trusted attributes + if strings.HasPrefix(key, "trusted.") { + return errors.Wrap(unix.ENOTSUP, "admin attributes from archive not supported") + } + return unix.Lsetxattr(path, key, []byte(value), 0) } -// apply applies a tar stream of an OCI style diff tar. -// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets -func apply(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { - return applyNaive(ctx, root, tr, options) +func copyDirInfo(fi os.FileInfo, path string) error { + st := fi.Sys().(*syscall.Stat_t) + if err := os.Lchown(path, int(st.Uid), int(st.Gid)); err != nil { + if os.IsPermission(err) { + // Normally if uid/gid are the same this would be a no-op, but some + // filesystems may still return EPERM... for instance NFS does this. + // In such a case, this is not an error. + if dstStat, err2 := os.Lstat(path); err2 == nil { + st2 := dstStat.Sys().(*syscall.Stat_t) + if st.Uid == st2.Uid && st.Gid == st2.Gid { + err = nil + } + } + } + if err != nil { + return errors.Wrapf(err, "failed to chown %s", path) + } + } + + if err := os.Chmod(path, fi.Mode()); err != nil { + return errors.Wrapf(err, "failed to chmod %s", path) + } + + timespec := []unix.Timespec{unix.Timespec(fs.StatAtime(st)), unix.Timespec(fs.StatMtime(st))} + if err := unix.UtimesNanoAt(unix.AT_FDCWD, path, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil { + return errors.Wrapf(err, "failed to utime %s", path) + } + + return nil +} + +func copyUpXAttrs(dst, src string) error { + xattrKeys, err := sysx.LListxattr(src) + if err != nil { + if err == unix.ENOTSUP || err == sysx.ENODATA { + return nil + } + return errors.Wrapf(err, "failed to list xattrs on %s", src) + } + for _, xattr := range xattrKeys { + // Do not copy up trusted attributes + if strings.HasPrefix(xattr, "trusted.") { + continue + } + data, err := sysx.LGetxattr(src, xattr) + if err != nil { + if err == unix.ENOTSUP || err == sysx.ENODATA { + continue + } + return errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src) + } + if err := unix.Lsetxattr(dst, xattr, data, unix.XATTR_CREATE); err != nil { + if err == unix.ENOTSUP || err == unix.ENODATA || err == unix.EEXIST { + continue + } + return errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst) + } + } + + return nil } diff --git a/archive/tar_windows.go b/archive/tar_windows.go index b97631fcc..a5c6da694 100644 --- a/archive/tar_windows.go +++ b/archive/tar_windows.go @@ -23,7 +23,6 @@ import ( "bufio" "context" "encoding/base64" - "errors" "fmt" "io" "os" @@ -36,6 +35,7 @@ import ( "github.com/Microsoft/go-winio" "github.com/Microsoft/hcsshim" "github.com/containerd/containerd/sys" + "github.com/pkg/errors" ) const ( @@ -107,10 +107,6 @@ func openFile(name string, flag int, perm os.FileMode) (*os.File, error) { return sys.OpenFileSequential(name, flag, perm) } -func mkdirAll(path string, perm os.FileMode) error { - return sys.MkdirAll(path, perm) -} - func mkdir(path string, perm os.FileMode) error { return os.Mkdir(path, perm) } @@ -153,16 +149,8 @@ func setxattr(path, key, value string) error { return errors.New("xattrs not supported on Windows") } -// apply applies a tar stream of an OCI style diff tar of a Windows layer. -// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets -func apply(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { - if options.IsWindowsContainerLayer { - return applyWindowsLayer(ctx, root, tr, options) - } - return applyNaive(ctx, root, tr, options) -} - -// applyWindowsLayer applies a tar stream of an OCI style diff tar of a Windows layer. +// applyWindowsLayer applies a tar stream of an OCI style diff tar of a Windows +// layer using the hcsshim layer writer and backup streams. // See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets func applyWindowsLayer(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { home, id := filepath.Split(root) @@ -170,7 +158,7 @@ func applyWindowsLayer(ctx context.Context, root string, tr *tar.Reader, options HomeDir: home, } - w, err := hcsshim.NewLayerWriter(info, id, options.ParentLayerPaths) + w, err := hcsshim.NewLayerWriter(info, id, options.Parents) if err != nil { return 0, err } @@ -443,3 +431,14 @@ func writeBackupStreamFromTarFile(w io.Writer, t *tar.Reader, hdr *tar.Header) ( } } } + +func copyDirInfo(fi os.FileInfo, path string) error { + if err := os.Chmod(path, fi.Mode()); err != nil { + return errors.Wrapf(err, "failed to chmod %s", path) + } + return nil +} + +func copyUpXAttrs(dst, src string) error { + return nil +} diff --git a/diff/apply/apply.go b/diff/apply/apply.go index 7a6b65c3e..97c065d8c 100644 --- a/diff/apply/apply.go +++ b/diff/apply/apply.go @@ -19,10 +19,8 @@ package apply import ( "context" "io" - "io/ioutil" "time" - "github.com/containerd/containerd/archive" "github.com/containerd/containerd/content" "github.com/containerd/containerd/diff" "github.com/containerd/containerd/log" @@ -94,15 +92,8 @@ func (s *fsApplier) Apply(ctx context.Context, desc ocispec.Descriptor, mounts [ rc := &readCounter{ r: io.TeeReader(processor, digester.Hash()), } - if err := mount.WithTempMount(ctx, mounts, func(root string) error { - if _, err := archive.Apply(ctx, root, rc); err != nil { - return err - } - // Read any trailing data - _, err := io.Copy(ioutil.Discard, rc) - return err - }); err != nil { + if err := apply(ctx, mounts, rc); err != nil { return emptyDesc, err } diff --git a/diff/apply/apply_linux.go b/diff/apply/apply_linux.go new file mode 100644 index 000000000..c36b6090c --- /dev/null +++ b/diff/apply/apply_linux.go @@ -0,0 +1,128 @@ +// +build linux + +/* + 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" + "strings" + + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/mount" + "github.com/pkg/errors" +) + +func apply(ctx context.Context, mounts []mount.Mount, r io.Reader) error { + switch { + case len(mounts) == 1 && mounts[0].Type == "overlay": + path, parents, err := getOverlayPath(mounts[0].Options) + if err != nil { + if errdefs.IsInvalidArgument(err) { + break + } + return err + } + opts := []archive.ApplyOpt{ + archive.WithConvertWhiteout(archive.OverlayConvertWhiteout), + } + if len(parents) > 0 { + opts = append(opts, archive.WithParents(parents)) + } + _, err = archive.Apply(ctx, path, r, opts...) + return err + case len(mounts) == 1 && mounts[0].Type == "aufs": + path, parents, err := getAufsPath(mounts[0].Options) + if err != nil { + if errdefs.IsInvalidArgument(err) { + break + } + return err + } + opts := []archive.ApplyOpt{ + archive.WithConvertWhiteout(archive.AufsConvertWhiteout), + } + if len(parents) > 0 { + opts = append(opts, archive.WithParents(parents)) + } + _, 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 + }) +} + +func getOverlayPath(options []string) (upper string, lower []string, err error) { + const upperdirPrefix = "upperdir=" + const lowerdirPrefix = "lowerdir=" + + for _, o := range options { + if strings.HasPrefix(o, upperdirPrefix) { + upper = strings.TrimPrefix(o, upperdirPrefix) + } else if strings.HasPrefix(o, lowerdirPrefix) { + lower = strings.Split(strings.TrimPrefix(o, lowerdirPrefix), ":") + } + } + if upper == "" { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "upperdir not found") + } + + return +} + +// getAufsPath handles options as given by the containerd aufs package only, +// formatted as "br:=rw[:=ro+wh]*" +func getAufsPath(options []string) (upper string, lower []string, err error) { + const ( + sep = ":" + brPrefix = "br:" + rwSuffix = "=rw" + roSuffix = "=ro+wh" + ) + for _, o := range options { + if strings.HasPrefix(o, brPrefix) { + o = strings.TrimPrefix(o, brPrefix) + } else { + continue + } + + for _, b := range strings.Split(o, sep) { + if strings.HasSuffix(b, rwSuffix) { + if upper != "" { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "multiple rw branch found") + } + upper = strings.TrimSuffix(b, rwSuffix) + } else if strings.HasSuffix(b, roSuffix) { + if upper == "" { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "rw branch be first") + } + lower = append(lower, strings.TrimSuffix(b, roSuffix)) + } else { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "unhandled aufs suffix") + } + + } + } + if upper == "" { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "rw branch not found") + } + return +} diff --git a/diff/apply/apply_linux_test.go b/diff/apply/apply_linux_test.go new file mode 100644 index 000000000..999d506c8 --- /dev/null +++ b/diff/apply/apply_linux_test.go @@ -0,0 +1,81 @@ +// +build linux + +/* + 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 ( + "testing" +) + +func TestGetOverlayPath(t *testing.T) { + good := []string{"upperdir=/test/upper", "lowerdir=/test/lower1:/test/lower2", "workdir=/test/work"} + path, parents, err := getOverlayPath(good) + if err != nil { + t.Fatalf("Get overlay path failed: %v", err) + } + if path != "/test/upper" { + t.Fatalf("Unexpected upperdir: %q", path) + } + if len(parents) != 2 || parents[0] != "/test/lower1" || parents[1] != "/test/lower2" { + t.Fatalf("Unexpected parents: %v", parents) + } + + bad := []string{"lowerdir=/test/lower"} + _, _, err = getOverlayPath(bad) + if err == nil { + t.Fatalf("An error is expected") + } +} + +func TestGetAufsPath(t *testing.T) { + for _, test := range []struct { + options []string + expectErr bool + }{ + { + options: []string{"random:option", "br:/test/rw=rw:/test/ro=ro+wh"}, + expectErr: false, + }, + { + options: []string{"random:option"}, + expectErr: true, + }, + { + options: []string{"br:/test/ro=ro+wh"}, + expectErr: true, + }, + } { + path, parents, err := getAufsPath(test.options) + if test.expectErr { + if err == nil { + t.Fatalf("An error is expected") + } + continue + } + if err != nil { + t.Fatalf("Get aufs path failed: %v", err) + } + if path != "/test/rw" { + t.Fatalf("Unexpected rw dir: %q", path) + } + if len(parents) != 1 || parents[0] != "/test/ro" { + t.Fatalf("Unexpected parents: %v", parents) + } + + } +} diff --git a/archive/tar_opts_unix.go b/diff/apply/apply_other.go similarity index 63% rename from archive/tar_opts_unix.go rename to diff/apply/apply_other.go index 173826967..01e0f11bb 100644 --- a/archive/tar_opts_unix.go +++ b/diff/apply/apply_other.go @@ -1,4 +1,4 @@ -// +build !windows +// +build !linux /* Copyright The containerd Authors. @@ -16,9 +16,19 @@ limitations under the License. */ -package archive +package apply -// ApplyOptions provides additional options for an Apply operation -type ApplyOptions struct { - Filter Filter // Filter tar headers +import ( + "context" + "io" + + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/mount" +) + +func apply(ctx context.Context, mounts []mount.Mount, r io.Reader) error { + return mount.WithTempMount(ctx, mounts, func(root string) error { + _, err := archive.Apply(ctx, root, r) + return err + }) } diff --git a/diff/windows/windows.go b/diff/windows/windows.go index ce584dc27..af193516b 100644 --- a/diff/windows/windows.go +++ b/diff/windows/windows.go @@ -139,7 +139,7 @@ func (s windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts return emptyDesc, err } - if _, err := archive.Apply(ctx, layer, rc, archive.WithParentLayers(parentLayerPaths), archive.AsWindowsContainerLayer()); err != nil { + if _, err := archive.Apply(ctx, layer, rc, archive.WithParents(parentLayerPaths), archive.AsWindowsContainerLayer()); err != nil { return emptyDesc, err } diff --git a/mount/lookup_linux_test.go b/mount/lookup_linux_test.go index 7ec9e3ad6..e9347566e 100644 --- a/mount/lookup_linux_test.go +++ b/mount/lookup_linux_test.go @@ -50,25 +50,25 @@ func testLookup(t *testing.T, fsType string) { } defer os.RemoveAll(mnt) - deviceName, cleanupDevice, err := loopback.New(100 << 20) // 100 MB + loop, err := loopback.New(100 << 20) // 100 MB if err != nil { t.Fatal(err) } - if out, err := exec.Command("mkfs", "-t", fsType, deviceName).CombinedOutput(); err != nil { + if out, err := exec.Command("mkfs", "-t", fsType, loop.Device).CombinedOutput(); err != nil { // not fatal - cleanupDevice() - t.Skipf("could not mkfs (%s) %s: %v (out: %q)", fsType, deviceName, err, string(out)) + loop.Close() + t.Skipf("could not mkfs (%s) %s: %v (out: %q)", fsType, loop.Device, err, string(out)) } - if out, err := exec.Command("mount", deviceName, mnt).CombinedOutput(); err != nil { + if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil { // not fatal - cleanupDevice() - t.Skipf("could not mount %s: %v (out: %q)", deviceName, err, string(out)) + loop.Close() + t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out)) } defer func() { testutil.Unmount(t, mnt) - cleanupDevice() + loop.Close() }() - assert.Check(t, strings.HasPrefix(deviceName, "/dev/loop")) + assert.Check(t, strings.HasPrefix(loop.Device, "/dev/loop")) checkLookup(t, fsType, mnt, mnt) newMnt, err := ioutil.TempDir("", "containerd-mountinfo-test-newMnt") diff --git a/snapshots/btrfs/btrfs_test.go b/snapshots/btrfs/btrfs_test.go index 6e5102356..842f80d83 100644 --- a/snapshots/btrfs/btrfs_test.go +++ b/snapshots/btrfs/btrfs_test.go @@ -52,24 +52,24 @@ func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshots.Snap if os.Getpagesize() > 4096 { loopbackSize = int64(650 << 20) // 650 MB } - deviceName, cleanupDevice, err := loopback.New(loopbackSize) + loop, err := loopback.New(loopbackSize) if err != nil { return nil, nil, err } - if out, err := exec.Command(mkbtrfs, deviceName).CombinedOutput(); err != nil { - cleanupDevice() + if out, err := exec.Command(mkbtrfs, loop.Device).CombinedOutput(); err != nil { + loop.Close() return nil, nil, errors.Wrapf(err, "failed to make btrfs filesystem (out: %q)", out) } - if out, err := exec.Command("mount", deviceName, root).CombinedOutput(); err != nil { - cleanupDevice() - return nil, nil, errors.Wrapf(err, "failed to mount device %s (out: %q)", deviceName, out) + if out, err := exec.Command("mount", loop.Device, root).CombinedOutput(); err != nil { + loop.Close() + return nil, nil, errors.Wrapf(err, "failed to mount device %s (out: %q)", loop.Device, out) } snapshotter, err := NewSnapshotter(root) if err != nil { - cleanupDevice() + loop.Close() return nil, nil, errors.Wrap(err, "failed to create new snapshotter") } @@ -78,7 +78,7 @@ func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshots.Snap return err } err := mount.UnmountAll(root, unix.MNT_DETACH) - if cerr := cleanupDevice(); cerr != nil { + if cerr := loop.Close(); cerr != nil { err = errors.Wrap(cerr, "device cleanup failed") } return err diff --git a/snapshots/overlay/check_test.go b/snapshots/overlay/check_test.go index 490145e01..16be50e90 100644 --- a/snapshots/overlay/check_test.go +++ b/snapshots/overlay/check_test.go @@ -36,23 +36,23 @@ func testOverlaySupported(t testing.TB, expected bool, mkfs ...string) { } defer os.RemoveAll(mnt) - deviceName, cleanupDevice, err := loopback.New(100 << 20) // 100 MB + loop, err := loopback.New(100 << 20) // 100 MB if err != nil { t.Fatal(err) } - if out, err := exec.Command(mkfs[0], append(mkfs[1:], deviceName)...).CombinedOutput(); err != nil { + if out, err := exec.Command(mkfs[0], append(mkfs[1:], loop.Device)...).CombinedOutput(); err != nil { // not fatal - cleanupDevice() - t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, deviceName, err, string(out)) + loop.Close() + t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, loop.Device, err, string(out)) } - if out, err := exec.Command("mount", deviceName, mnt).CombinedOutput(); err != nil { + if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil { // not fatal - cleanupDevice() - t.Skipf("could not mount %s: %v (out: %q)", deviceName, err, string(out)) + loop.Close() + t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out)) } defer func() { testutil.Unmount(t, mnt) - cleanupDevice() + loop.Close() }() workload := func() { err = Supported(mnt) diff --git a/vendor.conf b/vendor.conf index 37b41d293..0feeba16a 100644 --- a/vendor.conf +++ b/vendor.conf @@ -4,7 +4,7 @@ github.com/containerd/cgroups c4b9ac5c7601384c965b9646fc515884e091ebb9 github.com/containerd/typeurl a93fcdb778cd272c6e9b3028b2f42d813e785d40 github.com/containerd/fifo bda0ff6ed73c67bfb5e62bc9c697f146b7fd7f13 github.com/containerd/btrfs af5082808c833de0e79c1e72eea9fea239364877 -github.com/containerd/continuity bd77b46c8352f74eb12c85bdc01f4b90f69d66b4 +github.com/containerd/continuity f2a389ac0a02ce21c09edd7344677a601970f41c github.com/coreos/go-systemd 48702e0da86bd25e76cfef347e2adeb434a0d0a6 github.com/docker/go-metrics 4ea375f7759c82740c893fc030bc37088d2ec098 github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9 diff --git a/vendor/github.com/containerd/continuity/LICENSE b/vendor/github.com/containerd/continuity/LICENSE index 8f71f43fe..584149b6e 100644 --- a/vendor/github.com/containerd/continuity/LICENSE +++ b/vendor/github.com/containerd/continuity/LICENSE @@ -1,6 +1,7 @@ + Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,28 +176,16 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} + 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 + https://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. - diff --git a/vendor/github.com/containerd/continuity/README.md b/vendor/github.com/containerd/continuity/README.md index 0e91ce07b..f9f9ef0f9 100644 --- a/vendor/github.com/containerd/continuity/README.md +++ b/vendor/github.com/containerd/continuity/README.md @@ -72,3 +72,13 @@ If you change the proto file you will need to rebuild the generated Go with `go ```console $ go generate ./proto ``` + +## Project details + +continuity is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). +As a containerd sub-project, you will find the: + * [Project governance](https://github.com/containerd/project/blob/master/GOVERNANCE.md), + * [Maintainers](https://github.com/containerd/project/blob/master/MAINTAINERS), + * and [Contributing guidelines](https://github.com/containerd/project/blob/master/CONTRIBUTING.md) + +information in our [`containerd/project`](https://github.com/containerd/project) repository. diff --git a/vendor/github.com/containerd/continuity/fs/copy.go b/vendor/github.com/containerd/continuity/fs/copy.go index 42df6a9a5..ad61022ad 100644 --- a/vendor/github.com/containerd/continuity/fs/copy.go +++ b/vendor/github.com/containerd/continuity/fs/copy.go @@ -32,14 +32,49 @@ var bufferPool = &sync.Pool{ }, } -// CopyDir copies the directory from src to dst. -// Most efficient copy of files is attempted. -func CopyDir(dst, src string) error { - inodes := map[uint64]string{} - return copyDirectory(dst, src, inodes) +// XAttrErrorHandlers transform a non-nil xattr error. +// Return nil to ignore an error. +// xattrKey can be empty for listxattr operation. +type XAttrErrorHandler func(dst, src, xattrKey string, err error) error + +type copyDirOpts struct { + xeh XAttrErrorHandler } -func copyDirectory(dst, src string, inodes map[uint64]string) error { +type CopyDirOpt func(*copyDirOpts) error + +// WithXAttrErrorHandler allows specifying XAttrErrorHandler +// If nil XAttrErrorHandler is specified (default), CopyDir stops +// on a non-nil xattr error. +func WithXAttrErrorHandler(xeh XAttrErrorHandler) CopyDirOpt { + return func(o *copyDirOpts) error { + o.xeh = xeh + return nil + } +} + +// WithAllowXAttrErrors allows ignoring xattr errors. +func WithAllowXAttrErrors() CopyDirOpt { + xeh := func(dst, src, xattrKey string, err error) error { + return nil + } + return WithXAttrErrorHandler(xeh) +} + +// CopyDir copies the directory from src to dst. +// Most efficient copy of files is attempted. +func CopyDir(dst, src string, opts ...CopyDirOpt) error { + var o copyDirOpts + for _, opt := range opts { + if err := opt(&o); err != nil { + return err + } + } + inodes := map[uint64]string{} + return copyDirectory(dst, src, inodes, &o) +} + +func copyDirectory(dst, src string, inodes map[uint64]string, o *copyDirOpts) error { stat, err := os.Stat(src) if err != nil { return errors.Wrapf(err, "failed to stat %s", src) @@ -75,7 +110,7 @@ func copyDirectory(dst, src string, inodes map[uint64]string) error { switch { case fi.IsDir(): - if err := copyDirectory(target, source, inodes); err != nil { + if err := copyDirectory(target, source, inodes, o); err != nil { return err } continue @@ -111,7 +146,7 @@ func copyDirectory(dst, src string, inodes map[uint64]string) error { return errors.Wrap(err, "failed to copy file info") } - if err := copyXAttrs(target, source); err != nil { + if err := copyXAttrs(target, source, o.xeh); err != nil { return errors.Wrap(err, "failed to copy xattrs") } } diff --git a/vendor/github.com/containerd/continuity/fs/copy_linux.go b/vendor/github.com/containerd/continuity/fs/copy_linux.go index e041b5661..81c71522a 100644 --- a/vendor/github.com/containerd/continuity/fs/copy_linux.go +++ b/vendor/github.com/containerd/continuity/fs/copy_linux.go @@ -59,6 +59,8 @@ func copyFileInfo(fi os.FileInfo, name string) error { return nil } +const maxSSizeT = int64(^uint(0) >> 1) + func copyFileContent(dst, src *os.File) error { st, err := src.Stat() if err != nil { @@ -71,7 +73,16 @@ func copyFileContent(dst, src *os.File) error { dstFd := int(dst.Fd()) for size > 0 { - n, err := unix.CopyFileRange(srcFd, nil, dstFd, nil, int(size), 0) + // Ensure that we are never trying to copy more than SSIZE_MAX at a + // time and at the same time avoids overflows when the file is larger + // than 4GB on 32-bit systems. + var copySize int + if size > maxSSizeT { + copySize = int(maxSSizeT) + } else { + copySize = int(size) + } + n, err := unix.CopyFileRange(srcFd, nil, dstFd, nil, copySize, 0) if err != nil { if (err != unix.ENOSYS && err != unix.EXDEV) || !first { return errors.Wrap(err, "copy file range failed") @@ -90,18 +101,34 @@ func copyFileContent(dst, src *os.File) error { return nil } -func copyXAttrs(dst, src string) error { +func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error { xattrKeys, err := sysx.LListxattr(src) if err != nil { - return errors.Wrapf(err, "failed to list xattrs on %s", src) + e := errors.Wrapf(err, "failed to list xattrs on %s", src) + if xeh != nil { + e = xeh(dst, src, "", e) + } + return e } for _, xattr := range xattrKeys { data, err := sysx.LGetxattr(src, xattr) if err != nil { - return errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src) + e := errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src) + if xeh != nil { + if e = xeh(dst, src, xattr, e); e == nil { + continue + } + } + return e } if err := sysx.LSetxattr(dst, xattr, data, 0); err != nil { - return errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst) + e := errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst) + if xeh != nil { + if e = xeh(dst, src, xattr, e); e == nil { + continue + } + } + return e } } diff --git a/vendor/github.com/containerd/continuity/fs/copy_unix.go b/vendor/github.com/containerd/continuity/fs/copy_unix.go index 1a8ae5ebd..73c01a46d 100644 --- a/vendor/github.com/containerd/continuity/fs/copy_unix.go +++ b/vendor/github.com/containerd/continuity/fs/copy_unix.go @@ -69,18 +69,34 @@ func copyFileContent(dst, src *os.File) error { return err } -func copyXAttrs(dst, src string) error { +func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error { xattrKeys, err := sysx.LListxattr(src) if err != nil { - return errors.Wrapf(err, "failed to list xattrs on %s", src) + e := errors.Wrapf(err, "failed to list xattrs on %s", src) + if xeh != nil { + e = xeh(dst, src, "", e) + } + return e } for _, xattr := range xattrKeys { data, err := sysx.LGetxattr(src, xattr) if err != nil { - return errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src) + e := errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src) + if xeh != nil { + if e = xeh(dst, src, xattr, e); e == nil { + continue + } + } + return e } if err := sysx.LSetxattr(dst, xattr, data, 0); err != nil { - return errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst) + e := errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst) + if xeh != nil { + if e = xeh(dst, src, xattr, e); e == nil { + continue + } + } + return e } } diff --git a/vendor/github.com/containerd/continuity/fs/copy_windows.go b/vendor/github.com/containerd/continuity/fs/copy_windows.go index be8e6489b..27c7d7dbb 100644 --- a/vendor/github.com/containerd/continuity/fs/copy_windows.go +++ b/vendor/github.com/containerd/continuity/fs/copy_windows.go @@ -40,7 +40,7 @@ func copyFileContent(dst, src *os.File) error { return err } -func copyXAttrs(dst, src string) error { +func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error { return nil } diff --git a/vendor/github.com/containerd/continuity/fs/fstest/compare.go b/vendor/github.com/containerd/continuity/fs/fstest/compare.go index b61d83082..0d100b624 100644 --- a/vendor/github.com/containerd/continuity/fs/fstest/compare.go +++ b/vendor/github.com/containerd/continuity/fs/fstest/compare.go @@ -49,15 +49,7 @@ func CheckDirectoryEqual(d1, d2 string) error { diff := diffResourceList(m1.Resources, m2.Resources) if diff.HasDiff() { - if len(diff.Deletions) != 0 { - return errors.Errorf("directory diff between %s and %s\n%s", d1, d2, diff.String()) - } - // TODO: Also skip Recycle Bin contents in Windows layers which is used to store deleted files in some cases - for _, add := range diff.Additions { - if ok, _ := metadataFiles[add.Path()]; !ok { - return errors.Errorf("directory diff between %s and %s\n%s", d1, d2, diff.String()) - } - } + return errors.Errorf("directory diff between %s and %s\n%s", d1, d2, diff.String()) } return nil diff --git a/vendor/github.com/containerd/continuity/fs/fstest/compare_windows.go b/vendor/github.com/containerd/continuity/fs/fstest/compare_windows.go index 6b9104de0..a35781999 100644 --- a/vendor/github.com/containerd/continuity/fs/fstest/compare_windows.go +++ b/vendor/github.com/containerd/continuity/fs/fstest/compare_windows.go @@ -17,6 +17,7 @@ package fstest // TODO: Any more metadata files generated by Windows layers? +// TODO: Also skip Recycle Bin contents in Windows layers which is used to store deleted files in some cases var metadataFiles = map[string]bool{ "\\System Volume Information": true, "\\WcSandboxState": true, diff --git a/vendor/github.com/containerd/continuity/fs/fstest/continuity_util.go b/vendor/github.com/containerd/continuity/fs/fstest/continuity_util.go index 9cbfc0b6c..4d30dd01f 100644 --- a/vendor/github.com/containerd/continuity/fs/fstest/continuity_util.go +++ b/vendor/github.com/containerd/continuity/fs/fstest/continuity_util.go @@ -42,7 +42,17 @@ type resourceListDifference struct { } func (l resourceListDifference) HasDiff() bool { - return len(l.Additions) > 0 || len(l.Deletions) > 0 || len(l.Updates) > 0 + if len(l.Deletions) > 0 || len(l.Updates) > 0 || (len(metadataFiles) == 0 && len(l.Additions) > 0) { + return true + } + + for _, add := range l.Additions { + if ok, _ := metadataFiles[add.Path()]; !ok { + return true + } + } + + return false } func (l resourceListDifference) String() string { diff --git a/vendor/github.com/containerd/continuity/fs/path.go b/vendor/github.com/containerd/continuity/fs/path.go index 995981780..8863caa9d 100644 --- a/vendor/github.com/containerd/continuity/fs/path.go +++ b/vendor/github.com/containerd/continuity/fs/path.go @@ -22,7 +22,6 @@ import ( "io" "os" "path/filepath" - "strings" "github.com/pkg/errors" ) @@ -47,9 +46,8 @@ func pathChange(lower, upper *currentPath) (ChangeKind, string) { if upper == nil { return ChangeKindDelete, lower.path } - // TODO: compare by directory - switch i := strings.Compare(lower.path, upper.path); { + switch i := directoryCompare(lower.path, upper.path); { case i < 0: // File in lower that is not in upper return ChangeKindDelete, lower.path @@ -61,6 +59,35 @@ func pathChange(lower, upper *currentPath) (ChangeKind, string) { } } +func directoryCompare(a, b string) int { + l := len(a) + if len(b) < l { + l = len(b) + } + for i := 0; i < l; i++ { + c1, c2 := a[i], b[i] + if c1 == filepath.Separator { + c1 = byte(0) + } + if c2 == filepath.Separator { + c2 = byte(0) + } + if c1 < c2 { + return -1 + } + if c1 > c2 { + return +1 + } + } + if len(a) < len(b) { + return -1 + } + if len(a) > len(b) { + return +1 + } + return 0 +} + func sameFile(f1, f2 *currentPath) (bool, error) { if os.SameFile(f1.f, f2.f) { return true, nil diff --git a/vendor/github.com/containerd/continuity/testutil/loopback/loopback_linux.go b/vendor/github.com/containerd/continuity/testutil/loopback/loopback_linux.go index b17361170..c404c2699 100644 --- a/vendor/github.com/containerd/continuity/testutil/loopback/loopback_linux.go +++ b/vendor/github.com/containerd/continuity/testutil/loopback/loopback_linux.go @@ -22,24 +22,25 @@ import ( "io/ioutil" "os" "os/exec" + "syscall" "strings" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -// New creates a loopback device, and returns its device name (/dev/loopX), and its clean-up function. -func New(size int64) (string, func() error, error) { +// New creates a loopback device +func New(size int64) (*Loopback, error) { // create temporary file for the disk image file, err := ioutil.TempFile("", "containerd-test-loopback") if err != nil { - return "", nil, errors.Wrap(err, "could not create temporary file for loopback") + return nil, errors.Wrap(err, "could not create temporary file for loopback") } if err := file.Truncate(size); err != nil { file.Close() os.Remove(file.Name()) - return "", nil, errors.Wrap(err, "failed to resize temp file") + return nil, errors.Wrap(err, "failed to resize temp file") } file.Close() @@ -48,7 +49,7 @@ func New(size int64) (string, func() error, error) { p, err := losetup.Output() if err != nil { os.Remove(file.Name()) - return "", nil, errors.Wrap(err, "loopback setup failed") + return nil, errors.Wrap(err, "loopback setup failed") } deviceName := strings.TrimSpace(string(p)) @@ -68,5 +69,47 @@ func New(size int64) (string, func() error, error) { return os.Remove(file.Name()) } - return deviceName, cleanup, nil + l := Loopback{ + File: file.Name(), + Device: deviceName, + close: cleanup, + } + return &l, nil +} + +// Loopback device +type Loopback struct { + // File is the underlying sparse file + File string + // Device is /dev/loopX + Device string + close func() error +} + +// SoftSize returns st_size +func (l *Loopback) SoftSize() (int64, error) { + st, err := os.Stat(l.File) + if err != nil { + return 0, err + } + return st.Size(), nil +} + +// HardSize returns st_blocks * 512; see stat(2) +func (l *Loopback) HardSize() (int64, error) { + st, err := os.Stat(l.File) + if err != nil { + return 0, err + } + st2, ok := st.Sys().(*syscall.Stat_t) + if !ok { + return 0, errors.New("st.Sys() is not a *syscall.Stat_t") + } + // NOTE: st_blocks has nothing to do with st_blksize; see stat(2) + return st2.Blocks * 512, nil +} + +// Close detaches the device and removes the underlying file +func (l *Loopback) Close() error { + return l.close() }