update vendor

Signed-off-by: Jess Frazelle <acidburn@microsoft.com>
This commit is contained in:
Jess Frazelle 2018-09-25 12:27:46 -04:00
parent 19a32db84d
commit 94d1cfbfbf
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
10501 changed files with 2307943 additions and 29279 deletions

137
vendor/github.com/containerd/continuity/fs/copy.go generated vendored Normal file
View file

@ -0,0 +1,137 @@
/*
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 fs
import (
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/pkg/errors"
)
var bufferPool = &sync.Pool{
New: func() interface{} {
buffer := make([]byte, 32*1024)
return &buffer
},
}
// 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)
}
func copyDirectory(dst, src string, inodes map[uint64]string) error {
stat, err := os.Stat(src)
if err != nil {
return errors.Wrapf(err, "failed to stat %s", src)
}
if !stat.IsDir() {
return errors.Errorf("source is not directory")
}
if st, err := os.Stat(dst); err != nil {
if err := os.Mkdir(dst, stat.Mode()); err != nil {
return errors.Wrapf(err, "failed to mkdir %s", dst)
}
} else if !st.IsDir() {
return errors.Errorf("cannot copy to non-directory: %s", dst)
} else {
if err := os.Chmod(dst, stat.Mode()); err != nil {
return errors.Wrapf(err, "failed to chmod on %s", dst)
}
}
fis, err := ioutil.ReadDir(src)
if err != nil {
return errors.Wrapf(err, "failed to read %s", src)
}
if err := copyFileInfo(stat, dst); err != nil {
return errors.Wrapf(err, "failed to copy file info for %s", dst)
}
for _, fi := range fis {
source := filepath.Join(src, fi.Name())
target := filepath.Join(dst, fi.Name())
switch {
case fi.IsDir():
if err := copyDirectory(target, source, inodes); err != nil {
return err
}
continue
case (fi.Mode() & os.ModeType) == 0:
link, err := getLinkSource(target, fi, inodes)
if err != nil {
return errors.Wrap(err, "failed to get hardlink")
}
if link != "" {
if err := os.Link(link, target); err != nil {
return errors.Wrap(err, "failed to create hard link")
}
} else if err := CopyFile(target, source); err != nil {
return errors.Wrap(err, "failed to copy files")
}
case (fi.Mode() & os.ModeSymlink) == os.ModeSymlink:
link, err := os.Readlink(source)
if err != nil {
return errors.Wrapf(err, "failed to read link: %s", source)
}
if err := os.Symlink(link, target); err != nil {
return errors.Wrapf(err, "failed to create symlink: %s", target)
}
case (fi.Mode() & os.ModeDevice) == os.ModeDevice:
if err := copyDevice(target, fi); err != nil {
return errors.Wrapf(err, "failed to create device")
}
default:
// TODO: Support pipes and sockets
return errors.Wrapf(err, "unsupported mode %s", fi.Mode())
}
if err := copyFileInfo(fi, target); err != nil {
return errors.Wrap(err, "failed to copy file info")
}
if err := copyXAttrs(target, source); err != nil {
return errors.Wrap(err, "failed to copy xattrs")
}
}
return nil
}
// CopyFile copies the source file to the target.
// The most efficient means of copying is used for the platform.
func CopyFile(target, source string) error {
src, err := os.Open(source)
if err != nil {
return errors.Wrapf(err, "failed to open source %s", source)
}
defer src.Close()
tgt, err := os.Create(target)
if err != nil {
return errors.Wrapf(err, "failed to open target %s", target)
}
defer tgt.Close()
return copyFileContent(tgt, src)
}

View file

@ -0,0 +1,117 @@
/*
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 fs
import (
"io"
"os"
"syscall"
"github.com/containerd/continuity/sysx"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
func copyFileInfo(fi os.FileInfo, name string) error {
st := fi.Sys().(*syscall.Stat_t)
if err := os.Lchown(name, 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(name); 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", name)
}
}
if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink {
if err := os.Chmod(name, fi.Mode()); err != nil {
return errors.Wrapf(err, "failed to chmod %s", name)
}
}
timespec := []unix.Timespec{unix.Timespec(StatAtime(st)), unix.Timespec(StatMtime(st))}
if err := unix.UtimesNanoAt(unix.AT_FDCWD, name, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil {
return errors.Wrapf(err, "failed to utime %s", name)
}
return nil
}
func copyFileContent(dst, src *os.File) error {
st, err := src.Stat()
if err != nil {
return errors.Wrap(err, "unable to stat source")
}
size := st.Size()
first := true
srcFd := int(src.Fd())
dstFd := int(dst.Fd())
for size > 0 {
n, err := unix.CopyFileRange(srcFd, nil, dstFd, nil, int(size), 0)
if err != nil {
if (err != unix.ENOSYS && err != unix.EXDEV) || !first {
return errors.Wrap(err, "copy file range failed")
}
buf := bufferPool.Get().(*[]byte)
_, err = io.CopyBuffer(dst, src, *buf)
bufferPool.Put(buf)
return errors.Wrap(err, "userspace copy failed")
}
first = false
size -= int64(n)
}
return nil
}
func copyXAttrs(dst, src string) error {
xattrKeys, err := sysx.LListxattr(src)
if err != nil {
return errors.Wrapf(err, "failed to list xattrs on %s", src)
}
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)
}
if err := sysx.LSetxattr(dst, xattr, data, 0); err != nil {
return errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst)
}
}
return nil
}
func copyDevice(dst string, fi os.FileInfo) error {
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("unsupported stat type")
}
return unix.Mknod(dst, uint32(fi.Mode()), int(st.Rdev))
}

102
vendor/github.com/containerd/continuity/fs/copy_test.go generated vendored Normal file
View file

@ -0,0 +1,102 @@
/*
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 fs
import (
_ "crypto/sha256"
"io/ioutil"
"os"
"testing"
"time"
"github.com/containerd/continuity/fs/fstest"
"github.com/pkg/errors"
)
// TODO: Create copy directory which requires privilege
// chown
// mknod
// setxattr fstest.SetXAttr("/home", "trusted.overlay.opaque", "y"),
func TestCopyDirectory(t *testing.T) {
apply := fstest.Apply(
fstest.CreateDir("/etc/", 0755),
fstest.CreateFile("/etc/hosts", []byte("localhost 127.0.0.1"), 0644),
fstest.Link("/etc/hosts", "/etc/hosts.allow"),
fstest.CreateDir("/usr/local/lib", 0755),
fstest.CreateFile("/usr/local/lib/libnothing.so", []byte{0x00, 0x00}, 0755),
fstest.Symlink("libnothing.so", "/usr/local/lib/libnothing.so.2"),
fstest.CreateDir("/home", 0755),
)
if err := testCopy(apply); err != nil {
t.Fatalf("Copy test failed: %+v", err)
}
}
// This test used to fail because link-no-nothing.txt would be copied first,
// then file operations in dst during the CopyDir would follow the symlink and
// fail.
func TestCopyDirectoryWithLocalSymlink(t *testing.T) {
apply := fstest.Apply(
fstest.CreateFile("nothing.txt", []byte{0x00, 0x00}, 0755),
fstest.Symlink("nothing.txt", "link-no-nothing.txt"),
)
if err := testCopy(apply); err != nil {
t.Fatalf("Copy test failed: %+v", err)
}
}
// TestCopyWithLargeFile tests copying a file whose size > 2^32 bytes.
func TestCopyWithLargeFile(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
apply := fstest.Apply(
fstest.CreateDir("/banana", 0755),
fstest.CreateRandomFile("/banana/split", time.Now().UnixNano(), 3*1024*1024*1024, 0644),
)
if err := testCopy(apply); err != nil {
t.Fatal(err)
}
}
func testCopy(apply fstest.Applier) error {
t1, err := ioutil.TempDir("", "test-copy-src-")
if err != nil {
return errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(t1)
t2, err := ioutil.TempDir("", "test-copy-dst-")
if err != nil {
return errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(t2)
if err := apply.Apply(t1); err != nil {
return errors.Wrap(err, "failed to apply changes")
}
if err := CopyDir(t2, t1); err != nil {
return errors.Wrap(err, "failed to copy")
}
return fstest.CheckDirectoryEqual(t1, t2)
}

View file

@ -0,0 +1,96 @@
// +build solaris darwin freebsd
/*
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 fs
import (
"io"
"os"
"syscall"
"github.com/containerd/continuity/sysx"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
func copyFileInfo(fi os.FileInfo, name string) error {
st := fi.Sys().(*syscall.Stat_t)
if err := os.Lchown(name, 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(name); 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", name)
}
}
if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink {
if err := os.Chmod(name, fi.Mode()); err != nil {
return errors.Wrapf(err, "failed to chmod %s", name)
}
}
timespec := []syscall.Timespec{StatAtime(st), StatMtime(st)}
if err := syscall.UtimesNano(name, timespec); err != nil {
return errors.Wrapf(err, "failed to utime %s", name)
}
return nil
}
func copyFileContent(dst, src *os.File) error {
buf := bufferPool.Get().(*[]byte)
_, err := io.CopyBuffer(dst, src, *buf)
bufferPool.Put(buf)
return err
}
func copyXAttrs(dst, src string) error {
xattrKeys, err := sysx.LListxattr(src)
if err != nil {
return errors.Wrapf(err, "failed to list xattrs on %s", src)
}
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)
}
if err := sysx.LSetxattr(dst, xattr, data, 0); err != nil {
return errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst)
}
}
return nil
}
func copyDevice(dst string, fi os.FileInfo) error {
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("unsupported stat type")
}
return unix.Mknod(dst, uint32(fi.Mode()), int(st.Rdev))
}

View file

@ -0,0 +1,49 @@
/*
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 fs
import (
"io"
"os"
"github.com/pkg/errors"
)
func copyFileInfo(fi os.FileInfo, name string) error {
if err := os.Chmod(name, fi.Mode()); err != nil {
return errors.Wrapf(err, "failed to chmod %s", name)
}
// TODO: copy windows specific metadata
return nil
}
func copyFileContent(dst, src *os.File) error {
buf := bufferPool.Get().(*[]byte)
_, err := io.CopyBuffer(dst, src, *buf)
bufferPool.Put(buf)
return err
}
func copyXAttrs(dst, src string) error {
return nil
}
func copyDevice(dst string, fi os.FileInfo) error {
return errors.New("device copy not supported")
}

326
vendor/github.com/containerd/continuity/fs/diff.go generated vendored Normal file
View file

@ -0,0 +1,326 @@
/*
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 fs
import (
"context"
"os"
"path/filepath"
"strings"
"golang.org/x/sync/errgroup"
"github.com/sirupsen/logrus"
)
// ChangeKind is the type of modification that
// a change is making.
type ChangeKind int
const (
// ChangeKindUnmodified represents an unmodified
// file
ChangeKindUnmodified = iota
// ChangeKindAdd represents an addition of
// a file
ChangeKindAdd
// ChangeKindModify represents a change to
// an existing file
ChangeKindModify
// ChangeKindDelete represents a delete of
// a file
ChangeKindDelete
)
func (k ChangeKind) String() string {
switch k {
case ChangeKindUnmodified:
return "unmodified"
case ChangeKindAdd:
return "add"
case ChangeKindModify:
return "modify"
case ChangeKindDelete:
return "delete"
default:
return ""
}
}
// Change represents single change between a diff and its parent.
type Change struct {
Kind ChangeKind
Path string
}
// ChangeFunc is the type of function called for each change
// computed during a directory changes calculation.
type ChangeFunc func(ChangeKind, string, os.FileInfo, error) error
// Changes computes changes between two directories calling the
// given change function for each computed change. The first
// directory is intended to the base directory and second
// directory the changed directory.
//
// The change callback is called by the order of path names and
// should be appliable in that order.
// Due to this apply ordering, the following is true
// - Removed directory trees only create a single change for the root
// directory removed. Remaining changes are implied.
// - A directory which is modified to become a file will not have
// delete entries for sub-path items, their removal is implied
// by the removal of the parent directory.
//
// Opaque directories will not be treated specially and each file
// removed from the base directory will show up as a removal.
//
// File content comparisons will be done on files which have timestamps
// which may have been truncated. If either of the files being compared
// has a zero value nanosecond value, each byte will be compared for
// differences. If 2 files have the same seconds value but different
// nanosecond values where one of those values is zero, the files will
// be considered unchanged if the content is the same. This behavior
// is to account for timestamp truncation during archiving.
func Changes(ctx context.Context, a, b string, changeFn ChangeFunc) error {
if a == "" {
logrus.Debugf("Using single walk diff for %s", b)
return addDirChanges(ctx, changeFn, b)
} else if diffOptions := detectDirDiff(b, a); diffOptions != nil {
logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, a)
return diffDirChanges(ctx, changeFn, a, diffOptions)
}
logrus.Debugf("Using double walk diff for %s from %s", b, a)
return doubleWalkDiff(ctx, changeFn, a, b)
}
func addDirChanges(ctx context.Context, changeFn ChangeFunc, root string) error {
return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
// Rebase path
path, err = filepath.Rel(root, path)
if err != nil {
return err
}
path = filepath.Join(string(os.PathSeparator), path)
// Skip root
if path == string(os.PathSeparator) {
return nil
}
return changeFn(ChangeKindAdd, path, f, nil)
})
}
// diffDirOptions is used when the diff can be directly calculated from
// a diff directory to its base, without walking both trees.
type diffDirOptions struct {
diffDir string
skipChange func(string) (bool, error)
deleteChange func(string, string, os.FileInfo) (string, error)
}
// diffDirChanges walks the diff directory and compares changes against the base.
func diffDirChanges(ctx context.Context, changeFn ChangeFunc, base string, o *diffDirOptions) error {
changedDirs := make(map[string]struct{})
return filepath.Walk(o.diffDir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
// Rebase path
path, err = filepath.Rel(o.diffDir, path)
if err != nil {
return err
}
path = filepath.Join(string(os.PathSeparator), path)
// Skip root
if path == string(os.PathSeparator) {
return nil
}
// TODO: handle opaqueness, start new double walker at this
// location to get deletes, and skip tree in single walker
if o.skipChange != nil {
if skip, err := o.skipChange(path); skip {
return err
}
}
var kind ChangeKind
deletedFile, err := o.deleteChange(o.diffDir, path, f)
if err != nil {
return err
}
// Find out what kind of modification happened
if deletedFile != "" {
path = deletedFile
kind = ChangeKindDelete
f = nil
} else {
// Otherwise, the file was added
kind = ChangeKindAdd
// ...Unless it already existed in a base, in which case, it's a modification
stat, err := os.Stat(filepath.Join(base, path))
if err != nil && !os.IsNotExist(err) {
return err
}
if err == nil {
// The file existed in the base, so that's a modification
// However, if it's a directory, maybe it wasn't actually modified.
// If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar
if stat.IsDir() && f.IsDir() {
if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) {
// Both directories are the same, don't record the change
return nil
}
}
kind = ChangeKindModify
}
}
// If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files.
// This block is here to ensure the change is recorded even if the
// modify time, mode and size of the parent directory in the rw and ro layers are all equal.
// Check https://github.com/docker/docker/pull/13590 for details.
if f.IsDir() {
changedDirs[path] = struct{}{}
}
if kind == ChangeKindAdd || kind == ChangeKindDelete {
parent := filepath.Dir(path)
if _, ok := changedDirs[parent]; !ok && parent != "/" {
pi, err := os.Stat(filepath.Join(o.diffDir, parent))
if err := changeFn(ChangeKindModify, parent, pi, err); err != nil {
return err
}
changedDirs[parent] = struct{}{}
}
}
return changeFn(kind, path, f, nil)
})
}
// doubleWalkDiff walks both directories to create a diff
func doubleWalkDiff(ctx context.Context, changeFn ChangeFunc, a, b string) (err error) {
g, ctx := errgroup.WithContext(ctx)
var (
c1 = make(chan *currentPath)
c2 = make(chan *currentPath)
f1, f2 *currentPath
rmdir string
)
g.Go(func() error {
defer close(c1)
return pathWalk(ctx, a, c1)
})
g.Go(func() error {
defer close(c2)
return pathWalk(ctx, b, c2)
})
g.Go(func() error {
for c1 != nil || c2 != nil {
if f1 == nil && c1 != nil {
f1, err = nextPath(ctx, c1)
if err != nil {
return err
}
if f1 == nil {
c1 = nil
}
}
if f2 == nil && c2 != nil {
f2, err = nextPath(ctx, c2)
if err != nil {
return err
}
if f2 == nil {
c2 = nil
}
}
if f1 == nil && f2 == nil {
continue
}
var f os.FileInfo
k, p := pathChange(f1, f2)
switch k {
case ChangeKindAdd:
if rmdir != "" {
rmdir = ""
}
f = f2.f
f2 = nil
case ChangeKindDelete:
// Check if this file is already removed by being
// under of a removed directory
if rmdir != "" && strings.HasPrefix(f1.path, rmdir) {
f1 = nil
continue
} else if f1.f.IsDir() {
rmdir = f1.path + string(os.PathSeparator)
} else if rmdir != "" {
rmdir = ""
}
f1 = nil
case ChangeKindModify:
same, err := sameFile(f1, f2)
if err != nil {
return err
}
if f1.f.IsDir() && !f2.f.IsDir() {
rmdir = f1.path + string(os.PathSeparator)
} else if rmdir != "" {
rmdir = ""
}
f = f2.f
f1 = nil
f2 = nil
if same {
if !isLinked(f) {
continue
}
k = ChangeKindUnmodified
}
}
if err := changeFn(k, p, f, nil); err != nil {
return err
}
}
return nil
})
return g.Wait()
}

422
vendor/github.com/containerd/continuity/fs/diff_test.go generated vendored Normal file
View file

@ -0,0 +1,422 @@
/*
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 fs
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/containerd/continuity/fs/fstest"
"github.com/pkg/errors"
)
// TODO: Additional tests
// - capability test (requires privilege)
// - chown test (requires privilege)
// - symlink test
// - hardlink test
func skipDiffTestOnWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("diff implementation is incomplete on windows")
}
}
func TestSimpleDiff(t *testing.T) {
skipDiffTestOnWindows(t)
l1 := fstest.Apply(
fstest.CreateDir("/etc", 0755),
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
fstest.CreateFile("/etc/unchanged", []byte("PATH=/usr/bin"), 0644),
fstest.CreateFile("/etc/unexpected", []byte("#!/bin/sh"), 0644),
)
l2 := fstest.Apply(
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.120"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0666),
fstest.CreateDir("/root", 0700),
fstest.CreateFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
fstest.Remove("/etc/unexpected"),
)
diff := []TestChange{
Modify("/etc/hosts"),
Modify("/etc/profile"),
Delete("/etc/unexpected"),
Add("/root"),
Add("/root/.bashrc"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestNestedDeletion(t *testing.T) {
skipDiffTestOnWindows(t)
l1 := fstest.Apply(
fstest.CreateDir("/d0", 0755),
fstest.CreateDir("/d1", 0755),
fstest.CreateDir("/d1/d2", 0755),
fstest.CreateFile("/d1/d2/f1", []byte("mydomain 10.0.0.1"), 0644),
)
l2 := fstest.Apply(
fstest.RemoveAll("/d0"),
fstest.RemoveAll("/d1"),
)
diff := []TestChange{
Delete("/d0"),
Delete("/d1"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestDirectoryReplace(t *testing.T) {
skipDiffTestOnWindows(t)
l1 := fstest.Apply(
fstest.CreateDir("/dir1", 0755),
fstest.CreateFile("/dir1/f1", []byte("#####"), 0644),
fstest.CreateDir("/dir1/f2", 0755),
fstest.CreateFile("/dir1/f2/f3", []byte("#!/bin/sh"), 0644),
)
l2 := fstest.Apply(
fstest.CreateFile("/dir1/f11", []byte("#New file here"), 0644),
fstest.RemoveAll("/dir1/f2"),
fstest.CreateFile("/dir1/f2", []byte("Now file"), 0666),
)
diff := []TestChange{
Add("/dir1/f11"),
Modify("/dir1/f2"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestRemoveDirectoryTree(t *testing.T) {
l1 := fstest.Apply(
fstest.CreateDir("/dir1/dir2/dir3", 0755),
fstest.CreateFile("/dir1/f1", []byte("f1"), 0644),
fstest.CreateFile("/dir1/dir2/f2", []byte("f2"), 0644),
)
l2 := fstest.Apply(
fstest.RemoveAll("/dir1"),
)
diff := []TestChange{
Delete("/dir1"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestFileReplace(t *testing.T) {
l1 := fstest.Apply(
fstest.CreateFile("/dir1", []byte("a file, not a directory"), 0644),
)
l2 := fstest.Apply(
fstest.Remove("/dir1"),
fstest.CreateDir("/dir1/dir2", 0755),
fstest.CreateFile("/dir1/dir2/f1", []byte("also a file"), 0644),
)
diff := []TestChange{
Modify("/dir1"),
Add("/dir1/dir2"),
Add("/dir1/dir2/f1"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestParentDirectoryPermission(t *testing.T) {
skipDiffTestOnWindows(t)
l1 := fstest.Apply(
fstest.CreateDir("/dir1", 0700),
fstest.CreateDir("/dir2", 0751),
fstest.CreateDir("/dir3", 0777),
)
l2 := fstest.Apply(
fstest.CreateDir("/dir1/d", 0700),
fstest.CreateFile("/dir1/d/f", []byte("irrelevant"), 0644),
fstest.CreateFile("/dir1/f", []byte("irrelevant"), 0644),
fstest.CreateFile("/dir2/f", []byte("irrelevant"), 0644),
fstest.CreateFile("/dir3/f", []byte("irrelevant"), 0644),
)
diff := []TestChange{
Add("/dir1/d"),
Add("/dir1/d/f"),
Add("/dir1/f"),
Add("/dir2/f"),
Add("/dir3/f"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestUpdateWithSameTime(t *testing.T) {
skipDiffTestOnWindows(t)
tt := time.Now().Truncate(time.Second)
t1 := tt.Add(5 * time.Nanosecond)
t2 := tt.Add(6 * time.Nanosecond)
l1 := fstest.Apply(
fstest.CreateFile("/file-modified-time", []byte("1"), 0644),
fstest.Chtimes("/file-modified-time", t1, t1),
fstest.CreateFile("/file-no-change", []byte("1"), 0644),
fstest.Chtimes("/file-no-change", t1, t1),
fstest.CreateFile("/file-same-time", []byte("1"), 0644),
fstest.Chtimes("/file-same-time", t1, t1),
fstest.CreateFile("/file-truncated-time-1", []byte("1"), 0644),
fstest.Chtimes("/file-truncated-time-1", tt, tt),
fstest.CreateFile("/file-truncated-time-2", []byte("1"), 0644),
fstest.Chtimes("/file-truncated-time-2", tt, tt),
fstest.CreateFile("/file-truncated-time-3", []byte("1"), 0644),
fstest.Chtimes("/file-truncated-time-3", t1, t1),
)
l2 := fstest.Apply(
fstest.CreateFile("/file-modified-time", []byte("2"), 0644),
fstest.Chtimes("/file-modified-time", t2, t2),
fstest.CreateFile("/file-no-change", []byte("1"), 0644),
fstest.Chtimes("/file-no-change", t1, t1),
fstest.CreateFile("/file-same-time", []byte("2"), 0644),
fstest.Chtimes("/file-same-time", t1, t1),
fstest.CreateFile("/file-truncated-time-1", []byte("1"), 0644),
fstest.Chtimes("/file-truncated-time-1", t1, t1),
fstest.CreateFile("/file-truncated-time-2", []byte("2"), 0644),
fstest.Chtimes("/file-truncated-time-2", tt, tt),
fstest.CreateFile("/file-truncated-time-3", []byte("1"), 0644),
fstest.Chtimes("/file-truncated-time-3", tt, tt),
)
diff := []TestChange{
Modify("/file-modified-time"),
// Include changes with truncated timestamps. Comparing newly
// extracted tars which have truncated timestamps will be
// expected to produce changes. The expectation is that diff
// archives are generated once and kept, newly generated diffs
// will not consider cases where only one side is truncated.
Modify("/file-truncated-time-1"),
Modify("/file-truncated-time-2"),
Modify("/file-truncated-time-3"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
// buildkit#172
func TestLchtimes(t *testing.T) {
skipDiffTestOnWindows(t)
mtimes := []time.Time{
time.Unix(0, 0), // nsec is 0
time.Unix(0, 42), // nsec > 0
}
for _, mtime := range mtimes {
atime := time.Unix(424242, 42)
l1 := fstest.Apply(
fstest.CreateFile("/foo", []byte("foo"), 0644),
fstest.Symlink("/foo", "/lnk0"),
fstest.Lchtimes("/lnk0", atime, mtime),
)
l2 := fstest.Apply() // empty
diff := []TestChange{}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
}
func testDiffWithBase(base, diff fstest.Applier, expected []TestChange) error {
t1, err := ioutil.TempDir("", "diff-with-base-lower-")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(t1)
t2, err := ioutil.TempDir("", "diff-with-base-upper-")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(t2)
if err := base.Apply(t1); err != nil {
return errors.Wrap(err, "failed to apply base filesystem")
}
if err := CopyDir(t2, t1); err != nil {
return errors.Wrap(err, "failed to copy base directory")
}
if err := diff.Apply(t2); err != nil {
return errors.Wrap(err, "failed to apply diff filesystem")
}
changes, err := collectChanges(t1, t2)
if err != nil {
return errors.Wrap(err, "failed to collect changes")
}
return checkChanges(t2, changes, expected)
}
func TestBaseDirectoryChanges(t *testing.T) {
apply := fstest.Apply(
fstest.CreateDir("/etc", 0755),
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
fstest.CreateDir("/root", 0700),
fstest.CreateFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
)
changes := []TestChange{
Add("/etc"),
Add("/etc/hosts"),
Add("/etc/profile"),
Add("/root"),
Add("/root/.bashrc"),
}
if err := testDiffWithoutBase(apply, changes); err != nil {
t.Fatalf("Failed diff without base: %+v", err)
}
}
func testDiffWithoutBase(apply fstest.Applier, expected []TestChange) error {
tmp, err := ioutil.TempDir("", "diff-without-base-")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(tmp)
if err := apply.Apply(tmp); err != nil {
return errors.Wrap(err, "failed to apply filesytem changes")
}
changes, err := collectChanges("", tmp)
if err != nil {
return errors.Wrap(err, "failed to collect changes")
}
return checkChanges(tmp, changes, expected)
}
func checkChanges(root string, changes, expected []TestChange) error {
if len(changes) != len(expected) {
return errors.Errorf("Unexpected number of changes:\n%s", diffString(changes, expected))
}
for i := range changes {
if changes[i].Path != expected[i].Path || changes[i].Kind != expected[i].Kind {
return errors.Errorf("Unexpected change at %d:\n%s", i, diffString(changes, expected))
}
if changes[i].Kind != ChangeKindDelete {
filename := filepath.Join(root, changes[i].Path)
efi, err := os.Stat(filename)
if err != nil {
return errors.Wrapf(err, "failed to stat %q", filename)
}
afi := changes[i].FileInfo
if afi.Size() != efi.Size() {
return errors.Errorf("Unexpected change size %d, %q has size %d", afi.Size(), filename, efi.Size())
}
if afi.Mode() != efi.Mode() {
return errors.Errorf("Unexpected change mode %s, %q has mode %s", afi.Mode(), filename, efi.Mode())
}
if afi.ModTime() != efi.ModTime() {
return errors.Errorf("Unexpected change modtime %s, %q has modtime %s", afi.ModTime(), filename, efi.ModTime())
}
if expected := filepath.Join(root, changes[i].Path); changes[i].Source != expected {
return errors.Errorf("Unexpected source path %s, expected %s", changes[i].Source, expected)
}
}
}
return nil
}
type TestChange struct {
Kind ChangeKind
Path string
FileInfo os.FileInfo
Source string
}
func collectChanges(a, b string) ([]TestChange, error) {
changes := []TestChange{}
err := Changes(context.Background(), a, b, func(k ChangeKind, p string, f os.FileInfo, err error) error {
if err != nil {
return err
}
changes = append(changes, TestChange{
Kind: k,
Path: p,
FileInfo: f,
Source: filepath.Join(b, p),
})
return nil
})
if err != nil {
return nil, errors.Wrap(err, "failed to compute changes")
}
return changes, nil
}
func diffString(c1, c2 []TestChange) string {
return fmt.Sprintf("got(%d):\n%s\nexpected(%d):\n%s", len(c1), changesString(c1), len(c2), changesString(c2))
}
func changesString(c []TestChange) string {
strs := make([]string, len(c))
for i := range c {
strs[i] = fmt.Sprintf("\t%s\t%s", c[i].Kind, c[i].Path)
}
return strings.Join(strs, "\n")
}
func Add(p string) TestChange {
return TestChange{
Kind: ChangeKindAdd,
Path: filepath.FromSlash(p),
}
}
func Delete(p string) TestChange {
return TestChange{
Kind: ChangeKindDelete,
Path: filepath.FromSlash(p),
}
}
func Modify(p string) TestChange {
return TestChange{
Kind: ChangeKindModify,
Path: filepath.FromSlash(p),
}
}

View file

@ -0,0 +1,74 @@
// +build !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 fs
import (
"bytes"
"os"
"syscall"
"github.com/containerd/continuity/sysx"
"github.com/pkg/errors"
)
// detectDirDiff returns diff dir options if a directory could
// be found in the mount info for upper which is the direct
// diff with the provided lower directory
func detectDirDiff(upper, lower string) *diffDirOptions {
// TODO: get mount options for upper
// TODO: detect AUFS
// TODO: detect overlay
return nil
}
// compareSysStat returns whether the stats are equivalent,
// whether the files are considered the same file, and
// an error
func compareSysStat(s1, s2 interface{}) (bool, error) {
ls1, ok := s1.(*syscall.Stat_t)
if !ok {
return false, nil
}
ls2, ok := s2.(*syscall.Stat_t)
if !ok {
return false, nil
}
return ls1.Mode == ls2.Mode && ls1.Uid == ls2.Uid && ls1.Gid == ls2.Gid && ls1.Rdev == ls2.Rdev, nil
}
func compareCapabilities(p1, p2 string) (bool, error) {
c1, err := sysx.LGetxattr(p1, "security.capability")
if err != nil && err != sysx.ENODATA {
return false, errors.Wrapf(err, "failed to get xattr for %s", p1)
}
c2, err := sysx.LGetxattr(p2, "security.capability")
if err != nil && err != sysx.ENODATA {
return false, errors.Wrapf(err, "failed to get xattr for %s", p2)
}
return bytes.Equal(c1, c2), nil
}
func isLinked(f os.FileInfo) bool {
s, ok := f.Sys().(*syscall.Stat_t)
if !ok {
return false
}
return !f.IsDir() && s.Nlink > 1
}

View file

@ -0,0 +1,48 @@
/*
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 fs
import (
"os"
"golang.org/x/sys/windows"
)
func detectDirDiff(upper, lower string) *diffDirOptions {
return nil
}
func compareSysStat(s1, s2 interface{}) (bool, error) {
f1, ok := s1.(windows.Win32FileAttributeData)
if !ok {
return false, nil
}
f2, ok := s2.(windows.Win32FileAttributeData)
if !ok {
return false, nil
}
return f1.FileAttributes == f2.FileAttributes, nil
}
func compareCapabilities(p1, p2 string) (bool, error) {
// TODO: Use windows equivalent
return true, nil
}
func isLinked(os.FileInfo) bool {
return false
}

View file

@ -0,0 +1,103 @@
// +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 fs
import (
"fmt"
"io/ioutil"
"os"
"syscall"
"unsafe"
)
func locateDummyIfEmpty(path string) (string, error) {
children, err := ioutil.ReadDir(path)
if err != nil {
return "", err
}
if len(children) != 0 {
return "", nil
}
dummyFile, err := ioutil.TempFile(path, "fsutils-dummy")
if err != nil {
return "", err
}
name := dummyFile.Name()
err = dummyFile.Close()
return name, err
}
// SupportsDType returns whether the filesystem mounted on path supports d_type
func SupportsDType(path string) (bool, error) {
// locate dummy so that we have at least one dirent
dummy, err := locateDummyIfEmpty(path)
if err != nil {
return false, err
}
if dummy != "" {
defer os.Remove(dummy)
}
visited := 0
supportsDType := true
fn := func(ent *syscall.Dirent) bool {
visited++
if ent.Type == syscall.DT_UNKNOWN {
supportsDType = false
// stop iteration
return true
}
// continue iteration
return false
}
if err = iterateReadDir(path, fn); err != nil {
return false, err
}
if visited == 0 {
return false, fmt.Errorf("did not hit any dirent during iteration %s", path)
}
return supportsDType, nil
}
func iterateReadDir(path string, fn func(*syscall.Dirent) bool) error {
d, err := os.Open(path)
if err != nil {
return err
}
defer d.Close()
fd := int(d.Fd())
buf := make([]byte, 4096)
for {
nbytes, err := syscall.ReadDirent(fd, buf)
if err != nil {
return err
}
if nbytes == 0 {
break
}
for off := 0; off < nbytes; {
ent := (*syscall.Dirent)(unsafe.Pointer(&buf[off]))
if stop := fn(ent); stop {
return nil
}
off += int(ent.Reclen)
}
}
return nil
}

View file

@ -0,0 +1,75 @@
// +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 fs
import (
"io/ioutil"
"os"
"os/exec"
"testing"
"github.com/containerd/continuity/testutil"
)
func testSupportsDType(t *testing.T, expected bool, mkfs ...string) {
testutil.RequiresRoot(t)
mnt, err := ioutil.TempDir("", "containerd-fs-test-supports-dtype")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(mnt)
deviceName, cleanupDevice, err := testutil.NewLoopback(100 << 20) // 100 MB
if err != nil {
t.Fatal(err)
}
if out, err := exec.Command(mkfs[0], append(mkfs[1:], deviceName)...).CombinedOutput(); err != nil {
// not fatal
t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, deviceName, err, string(out))
}
if out, err := exec.Command("mount", deviceName, mnt).CombinedOutput(); err != nil {
// not fatal
t.Skipf("could not mount %s: %v (out: %q)", deviceName, err, string(out))
}
defer func() {
testutil.Unmount(t, mnt)
cleanupDevice()
}()
// check whether it supports d_type
result, err := SupportsDType(mnt)
if err != nil {
t.Fatal(err)
}
t.Logf("Supports d_type: %v", result)
if expected != result {
t.Fatalf("expected %+v, got: %+v", expected, result)
}
}
func TestSupportsDTypeWithFType0XFS(t *testing.T) {
testSupportsDType(t, false, "mkfs.xfs", "-m", "crc=0", "-n", "ftype=0")
}
func TestSupportsDTypeWithFType1XFS(t *testing.T) {
testSupportsDType(t, true, "mkfs.xfs", "-m", "crc=0", "-n", "ftype=1")
}
func TestSupportsDTypeWithExt4(t *testing.T) {
testSupportsDType(t, true, "mkfs.ext4", "-F")
}

View file

@ -0,0 +1,42 @@
/*
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 fs
import (
"testing"
"github.com/containerd/continuity/testutil"
)
func TestRequiresRootNOP(t *testing.T) {
// This is a dummy test case that exist to call
// testutil.RequiresRoot() on non-linux platforms. This is
// needed because the Makfile root-coverage tests target
// determines which packages contain root test by grepping for
// testutil.RequiresRoot. Within the fs package, the only test
// that references this symbol is in dtype_linux_test.go, but
// that file is only built on linux. Since the Makefile is not
// go build tag aware it sees this file and then tries to run
// the following command on all platforms: "go test ...
// github.com/containerd/continuity/fs -test.root". On
// non-linux platforms this fails because there are no tests in
// the "fs" package that reference testutil.RequiresRoot. To
// fix this problem we'll add a reference to this symbol below.
testutil.RequiresRoot(t)
}

38
vendor/github.com/containerd/continuity/fs/du.go generated vendored Normal file
View file

@ -0,0 +1,38 @@
/*
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 fs
import "context"
// Usage of disk information
type Usage struct {
Inodes int64
Size int64
}
// DiskUsage counts the number of inodes and disk usage for the resources under
// path.
func DiskUsage(ctx context.Context, roots ...string) (Usage, error) {
return diskUsage(ctx, roots...)
}
// DiffUsage counts the numbers of inodes and disk usage in the
// diff between the 2 directories. The first path is intended
// as the base directory and the second as the changed directory.
func DiffUsage(ctx context.Context, a, b string) (Usage, error) {
return diffUsage(ctx, a, b)
}

110
vendor/github.com/containerd/continuity/fs/du_unix.go generated vendored Normal file
View file

@ -0,0 +1,110 @@
// +build !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 fs
import (
"context"
"os"
"path/filepath"
"syscall"
)
type inode struct {
// TODO(stevvooe): Can probably reduce memory usage by not tracking
// device, but we can leave this right for now.
dev, ino uint64
}
func newInode(stat *syscall.Stat_t) inode {
return inode{
// Dev is uint32 on darwin/bsd, uint64 on linux/solaris
dev: uint64(stat.Dev), // nolint: unconvert
// Ino is uint32 on bsd, uint64 on darwin/linux/solaris
ino: uint64(stat.Ino), // nolint: unconvert
}
}
func diskUsage(ctx context.Context, roots ...string) (Usage, error) {
var (
size int64
inodes = map[inode]struct{}{} // expensive!
)
for _, root := range roots {
if err := filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
}
inoKey := newInode(fi.Sys().(*syscall.Stat_t))
if _, ok := inodes[inoKey]; !ok {
inodes[inoKey] = struct{}{}
size += fi.Size()
}
return nil
}); err != nil {
return Usage{}, err
}
}
return Usage{
Inodes: int64(len(inodes)),
Size: size,
}, nil
}
func diffUsage(ctx context.Context, a, b string) (Usage, error) {
var (
size int64
inodes = map[inode]struct{}{} // expensive!
)
if err := Changes(ctx, a, b, func(kind ChangeKind, _ string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if kind == ChangeKindAdd || kind == ChangeKindModify {
inoKey := newInode(fi.Sys().(*syscall.Stat_t))
if _, ok := inodes[inoKey]; !ok {
inodes[inoKey] = struct{}{}
size += fi.Size()
}
return nil
}
return nil
}); err != nil {
return Usage{}, err
}
return Usage{
Inodes: int64(len(inodes)),
Size: size,
}, nil
}

View file

@ -0,0 +1,82 @@
// +build 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 fs
import (
"context"
"os"
"path/filepath"
)
func diskUsage(ctx context.Context, roots ...string) (Usage, error) {
var (
size int64
)
// TODO(stevvooe): Support inodes (or equivalent) for windows.
for _, root := range roots {
if err := filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
}
size += fi.Size()
return nil
}); err != nil {
return Usage{}, err
}
}
return Usage{
Size: size,
}, nil
}
func diffUsage(ctx context.Context, a, b string) (Usage, error) {
var (
size int64
)
if err := Changes(ctx, a, b, func(kind ChangeKind, _ string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if kind == ChangeKindAdd || kind == ChangeKindModify {
size += fi.Size()
return nil
}
return nil
}); err != nil {
return Usage{}, err
}
return Usage{
Size: size,
}, nil
}

View file

@ -0,0 +1,77 @@
/*
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 fstest
import (
"io/ioutil"
"os"
"github.com/containerd/continuity"
"github.com/pkg/errors"
)
// CheckDirectoryEqual compares two directory paths to make sure that
// the content of the directories is the same.
func CheckDirectoryEqual(d1, d2 string) error {
c1, err := continuity.NewContext(d1)
if err != nil {
return errors.Wrap(err, "failed to build context")
}
c2, err := continuity.NewContext(d2)
if err != nil {
return errors.Wrap(err, "failed to build context")
}
m1, err := continuity.BuildManifest(c1)
if err != nil {
return errors.Wrap(err, "failed to build manifest")
}
m2, err := continuity.BuildManifest(c2)
if err != nil {
return errors.Wrap(err, "failed to build manifest")
}
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 nil
}
// CheckDirectoryEqualWithApplier compares directory against applier
func CheckDirectoryEqualWithApplier(root string, a Applier) error {
applied, err := ioutil.TempDir("", "fstest")
if err != nil {
return err
}
defer os.RemoveAll(applied)
if err := a.Apply(applied); err != nil {
return err
}
return CheckDirectoryEqual(applied, root)
}

View file

@ -0,0 +1,21 @@
// +build !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 fstest
var metadataFiles map[string]bool

View file

@ -0,0 +1,23 @@
/*
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 fstest
// TODO: Any more metadata files generated by Windows layers?
var metadataFiles = map[string]bool{
"\\System Volume Information": true,
"\\WcSandboxState": true,
}

View file

@ -0,0 +1,205 @@
/*
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 fstest
import (
"bytes"
"fmt"
"github.com/containerd/continuity"
)
type resourceUpdate struct {
Original continuity.Resource
Updated continuity.Resource
}
func (u resourceUpdate) String() string {
return fmt.Sprintf("%s(mode: %o, uid: %d, gid: %d) -> %s(mode: %o, uid: %d, gid: %d)",
u.Original.Path(), u.Original.Mode(), u.Original.UID(), u.Original.GID(),
u.Updated.Path(), u.Updated.Mode(), u.Updated.UID(), u.Updated.GID(),
)
}
type resourceListDifference struct {
Additions []continuity.Resource
Deletions []continuity.Resource
Updates []resourceUpdate
}
func (l resourceListDifference) HasDiff() bool {
return len(l.Additions) > 0 || len(l.Deletions) > 0 || len(l.Updates) > 0
}
func (l resourceListDifference) String() string {
buf := bytes.NewBuffer(nil)
for _, add := range l.Additions {
fmt.Fprintf(buf, "+ %s\n", add.Path())
}
for _, del := range l.Deletions {
fmt.Fprintf(buf, "- %s\n", del.Path())
}
for _, upt := range l.Updates {
fmt.Fprintf(buf, "~ %s\n", upt.String())
}
return string(buf.Bytes())
}
// diffManifest compares two resource lists and returns the list
// of adds updates and deletes, resource lists are not reordered
// before doing difference.
func diffResourceList(r1, r2 []continuity.Resource) resourceListDifference {
i1 := 0
i2 := 0
var d resourceListDifference
for i1 < len(r1) && i2 < len(r2) {
p1 := r1[i1].Path()
p2 := r2[i2].Path()
switch {
case p1 < p2:
d.Deletions = append(d.Deletions, r1[i1])
i1++
case p1 == p2:
if !compareResource(r1[i1], r2[i2]) {
d.Updates = append(d.Updates, resourceUpdate{
Original: r1[i1],
Updated: r2[i2],
})
}
i1++
i2++
case p1 > p2:
d.Additions = append(d.Additions, r2[i2])
i2++
}
}
for i1 < len(r1) {
d.Deletions = append(d.Deletions, r1[i1])
i1++
}
for i2 < len(r2) {
d.Additions = append(d.Additions, r2[i2])
i2++
}
return d
}
func compareResource(r1, r2 continuity.Resource) bool {
if r1.Path() != r2.Path() {
return false
}
if r1.Mode() != r2.Mode() {
return false
}
if r1.UID() != r2.UID() {
return false
}
if r1.GID() != r2.GID() {
return false
}
// TODO(dmcgowan): Check if is XAttrer
return compareResourceTypes(r1, r2)
}
func compareResourceTypes(r1, r2 continuity.Resource) bool {
switch t1 := r1.(type) {
case continuity.RegularFile:
t2, ok := r2.(continuity.RegularFile)
if !ok {
return false
}
return compareRegularFile(t1, t2)
case continuity.Directory:
t2, ok := r2.(continuity.Directory)
if !ok {
return false
}
return compareDirectory(t1, t2)
case continuity.SymLink:
t2, ok := r2.(continuity.SymLink)
if !ok {
return false
}
return compareSymLink(t1, t2)
case continuity.NamedPipe:
t2, ok := r2.(continuity.NamedPipe)
if !ok {
return false
}
return compareNamedPipe(t1, t2)
case continuity.Device:
t2, ok := r2.(continuity.Device)
if !ok {
return false
}
return compareDevice(t1, t2)
default:
// TODO(dmcgowan): Should this panic?
return r1 == r2
}
}
func compareRegularFile(r1, r2 continuity.RegularFile) bool {
if r1.Size() != r2.Size() {
return false
}
p1 := r1.Paths()
p2 := r2.Paths()
if len(p1) != len(p2) {
return false
}
for i := range p1 {
if p1[i] != p2[i] {
return false
}
}
d1 := r1.Digests()
d2 := r2.Digests()
if len(d1) != len(d2) {
return false
}
for i := range d1 {
if d1[i] != d2[i] {
return false
}
}
return true
}
func compareSymLink(r1, r2 continuity.SymLink) bool {
return r1.Target() == r2.Target()
}
func compareDirectory(r1, r2 continuity.Directory) bool {
return true
}
func compareNamedPipe(r1, r2 continuity.NamedPipe) bool {
return true
}
func compareDevice(r1, r2 continuity.Device) bool {
return r1.Major() == r2.Major() && r1.Minor() == r2.Minor()
}

View file

@ -0,0 +1,180 @@
/*
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 fstest
import (
"bytes"
"io"
"math/rand"
"net"
"os"
"path/filepath"
"time"
)
// Applier applies single file changes
type Applier interface {
Apply(root string) error
}
type applyFn func(root string) error
func (a applyFn) Apply(root string) error {
return a(root)
}
// CreateFile returns a file applier which creates a file as the
// provided name with the given content and permission.
func CreateFile(name string, content []byte, perm os.FileMode) Applier {
f := func() io.Reader {
return bytes.NewReader(content)
}
return writeFileStream(name, f, perm)
}
// CreateRandomFile returns a file applier which creates a file with random
// content of the given size using the given seed and permission.
func CreateRandomFile(name string, seed, size int64, perm os.FileMode) Applier {
f := func() io.Reader {
return io.LimitReader(rand.New(rand.NewSource(seed)), size)
}
return writeFileStream(name, f, perm)
}
// writeFileStream returns a file applier which creates a file as the
// provided name with the given content from the provided i/o stream and permission.
func writeFileStream(name string, stream func() io.Reader, perm os.FileMode) Applier {
return applyFn(func(root string) (retErr error) {
fullPath := filepath.Join(root, name)
f, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
defer func() {
err := f.Close()
if err != nil && retErr == nil {
retErr = err
}
}()
_, err = io.Copy(f, stream())
if err != nil {
return err
}
return os.Chmod(fullPath, perm)
})
}
// Remove returns a file applier which removes the provided file name
func Remove(name string) Applier {
return applyFn(func(root string) error {
return os.Remove(filepath.Join(root, name))
})
}
// RemoveAll returns a file applier which removes the provided file name
// as in os.RemoveAll
func RemoveAll(name string) Applier {
return applyFn(func(root string) error {
return os.RemoveAll(filepath.Join(root, name))
})
}
// CreateDir returns a file applier to create the directory with
// the provided name and permission
func CreateDir(name string, perm os.FileMode) Applier {
return applyFn(func(root string) error {
fullPath := filepath.Join(root, name)
if err := os.MkdirAll(fullPath, perm); err != nil {
return err
}
return os.Chmod(fullPath, perm)
})
}
// Rename returns a file applier which renames a file
func Rename(old, new string) Applier {
return applyFn(func(root string) error {
return os.Rename(filepath.Join(root, old), filepath.Join(root, new))
})
}
// Chown returns a file applier which changes the ownership of a file
func Chown(name string, uid, gid int) Applier {
return applyFn(func(root string) error {
return os.Chown(filepath.Join(root, name), uid, gid)
})
}
// Chtimes changes access and mod time of file.
// Use Lchtimes for symbolic links.
func Chtimes(name string, atime, mtime time.Time) Applier {
return applyFn(func(root string) error {
return os.Chtimes(filepath.Join(root, name), atime, mtime)
})
}
// Chmod returns a file applier which changes the file permission
func Chmod(name string, perm os.FileMode) Applier {
return applyFn(func(root string) error {
return os.Chmod(filepath.Join(root, name), perm)
})
}
// Symlink returns a file applier which creates a symbolic link
func Symlink(oldname, newname string) Applier {
return applyFn(func(root string) error {
return os.Symlink(oldname, filepath.Join(root, newname))
})
}
// Link returns a file applier which creates a hard link
func Link(oldname, newname string) Applier {
return applyFn(func(root string) error {
return os.Link(filepath.Join(root, oldname), filepath.Join(root, newname))
})
}
// TODO: Make platform specific, windows applier is always no-op
//func Mknod(name string, mode int32, dev int) Applier {
// return func(root string) error {
// return return syscall.Mknod(path, mode, dev)
// }
//}
func CreateSocket(name string, perm os.FileMode) Applier {
return applyFn(func(root string) error {
fullPath := filepath.Join(root, name)
ln, err := net.Listen("unix", fullPath)
if err != nil {
return err
}
defer ln.Close()
return os.Chmod(fullPath, perm)
})
}
// Apply returns a new applier from the given appliers
func Apply(appliers ...Applier) Applier {
return applyFn(func(root string) error {
for _, a := range appliers {
if err := a.Apply(root); err != nil {
return err
}
}
return nil
})
}

View file

@ -0,0 +1,52 @@
// +build !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 fstest
import (
"path/filepath"
"time"
"github.com/containerd/continuity/sysx"
"golang.org/x/sys/unix"
)
// SetXAttr sets the xatter for the file
func SetXAttr(name, key, value string) Applier {
return applyFn(func(root string) error {
return sysx.LSetxattr(name, key, []byte(value), 0)
})
}
// Lchtimes changes access and mod time of file without following symlink
func Lchtimes(name string, atime, mtime time.Time) Applier {
return applyFn(func(root string) error {
path := filepath.Join(root, name)
at := unix.NsecToTimespec(atime.UnixNano())
mt := unix.NsecToTimespec(mtime.UnixNano())
utimes := [2]unix.Timespec{at, mt}
return unix.UtimesNanoAt(unix.AT_FDCWD, path, utimes[0:], unix.AT_SYMLINK_NOFOLLOW)
})
}
func Base() Applier {
return applyFn(func(root string) error {
// do nothing, as the base is not special
return nil
})
}

View file

@ -0,0 +1,45 @@
/*
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 fstest
import (
"time"
"github.com/pkg/errors"
)
// Lchtimes changes access and mod time of file without following symlink
func Lchtimes(name string, atime, mtime time.Time) Applier {
return applyFn(func(root string) error {
return errors.New("Not implemented")
})
}
// Base applies the files required to make a valid Windows container layer
// that the filter will mount. It is used for testing the snapshotter
func Base() Applier {
return Apply(
CreateDir("Windows", 0755),
CreateDir("Windows/System32", 0755),
CreateDir("Windows/System32/Config", 0755),
CreateFile("Windows/System32/Config/SYSTEM", []byte("foo\n"), 0777),
CreateFile("Windows/System32/Config/SOFTWARE", []byte("foo\n"), 0777),
CreateFile("Windows/System32/Config/SAM", []byte("foo\n"), 0777),
CreateFile("Windows/System32/Config/SECURITY", []byte("foo\n"), 0777),
CreateFile("Windows/System32/Config/DEFAULT", []byte("foo\n"), 0777),
)
}

View file

@ -0,0 +1,237 @@
/*
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 fstest
import (
"context"
"io/ioutil"
"os"
"testing"
)
// TestApplier applies the test context
type TestApplier interface {
TestContext(context.Context) (context.Context, func(), error)
Apply(context.Context, Applier) (string, func(), error)
}
// FSSuite runs the path test suite
func FSSuite(t *testing.T, a TestApplier) {
t.Run("Basic", makeTest(t, a, basicTest))
t.Run("Deletion", makeTest(t, a, deletionTest))
t.Run("Update", makeTest(t, a, updateTest))
t.Run("DirectoryPermission", makeTest(t, a, directoryPermissionsTest))
t.Run("ParentDirectoryPermission", makeTest(t, a, parentDirectoryPermissionsTest))
t.Run("HardlinkUnmodified", makeTest(t, a, hardlinkUnmodified))
t.Run("HardlinkBeforeUnmodified", makeTest(t, a, hardlinkBeforeUnmodified))
t.Run("HardlinkBeforeModified", makeTest(t, a, hardlinkBeforeModified))
}
func makeTest(t *testing.T, ta TestApplier, as []Applier) func(t *testing.T) {
return func(t *testing.T) {
ctx, cleanup, err := ta.TestContext(context.Background())
if err != nil {
t.Fatalf("Unable to get test context: %+v", err)
}
defer cleanup()
applyDir, err := ioutil.TempDir("", "test-expected-")
if err != nil {
t.Fatalf("Unable to make temp directory: %+v", err)
}
defer os.RemoveAll(applyDir)
for i, a := range as {
testDir, c, err := ta.Apply(ctx, a)
if err != nil {
t.Fatalf("Apply failed at %d: %+v", i, err)
}
if err := a.Apply(applyDir); err != nil {
if c != nil {
c()
}
t.Fatalf("Error applying change to apply directory: %+v", err)
}
err = CheckDirectoryEqual(applyDir, testDir)
if c != nil {
c()
}
if err != nil {
t.Fatalf("Directories not equal at %d (expected <> tested): %+v", i, err)
}
}
}
}
var (
// baseApplier creates a basic filesystem layout
// with multiple types of files for basic tests.
baseApplier = Apply(
CreateDir("/etc/", 0755),
CreateFile("/etc/hosts", []byte("127.0.0.1 localhost"), 0644),
Link("/etc/hosts", "/etc/hosts.allow"),
CreateDir("/usr/local/lib", 0755),
CreateFile("/usr/local/lib/libnothing.so", []byte{0x00, 0x00}, 0755),
Symlink("libnothing.so", "/usr/local/lib/libnothing.so.2"),
CreateDir("/home", 0755),
CreateDir("/home/derek", 0700),
CreateDir("/var/run/socket", 0700),
)
// basicTest covers basic operations
basicTest = []Applier{
baseApplier,
Apply(
CreateFile("/etc/hosts", []byte("127.0.0.1 localhost.localdomain"), 0644),
CreateFile("/etc/fstab", []byte("/dev/sda1\t/\text4\tdefaults 1 1\n"), 0600),
CreateFile("/etc/badfile", []byte(""), 0666),
CreateFile("/home/derek/.zshrc", []byte("#ZSH is just better\n"), 0640),
),
Apply(
Remove("/etc/badfile"),
Rename("/home/derek", "/home/notderek"),
),
Apply(
RemoveAll("/usr"),
Remove("/etc/hosts.allow"),
),
Apply(
RemoveAll("/home"),
CreateDir("/home/derek", 0700),
CreateFile("/home/derek/.bashrc", []byte("#not going away\n"), 0640),
Link("/etc/hosts", "/etc/hosts.allow"),
),
}
// deletionTest covers various deletion scenarios to ensure
// deletions are properly picked up and applied
deletionTest = []Applier{
Apply(
CreateDir("/test/somedir", 0755),
CreateDir("/lib", 0700),
CreateFile("/lib/hidden", []byte{}, 0644),
),
Apply(
CreateFile("/test/a", []byte{}, 0644),
CreateFile("/test/b", []byte{}, 0644),
CreateDir("/test/otherdir", 0755),
CreateFile("/test/otherdir/.empty", []byte{}, 0644),
RemoveAll("/lib"),
CreateDir("/lib", 0700),
CreateFile("/lib/not-hidden", []byte{}, 0644),
),
Apply(
Remove("/test/a"),
Remove("/test/b"),
RemoveAll("/test/otherdir"),
CreateFile("/lib/newfile", []byte{}, 0644),
),
}
// updateTest covers file updates for content and permission
updateTest = []Applier{
Apply(
CreateDir("/d1", 0755),
CreateDir("/d2", 0700),
CreateFile("/d1/f1", []byte("something..."), 0644),
CreateFile("/d1/f2", []byte("else..."), 0644),
CreateFile("/d1/f3", []byte("entirely..."), 0644),
),
Apply(
CreateFile("/d1/f1", []byte("file content of a different length"), 0664),
Remove("/d1/f3"),
CreateFile("/d1/f3", []byte("updated content"), 0664),
Chmod("/d1/f2", 0766),
Chmod("/d2", 0777),
),
}
// directoryPermissionsTest covers directory permissions on update
directoryPermissionsTest = []Applier{
Apply(
CreateDir("/d1", 0700),
CreateDir("/d2", 0751),
CreateDir("/d3", 0777),
),
Apply(
CreateFile("/d1/f", []byte("irrelevant"), 0644),
CreateDir("/d1/d", 0700),
CreateFile("/d1/d/f", []byte("irrelevant"), 0644),
CreateFile("/d2/f", []byte("irrelevant"), 0644),
CreateFile("/d3/f", []byte("irrelevant"), 0644),
),
}
// parentDirectoryPermissionsTest covers directory permissions for updated
// files
parentDirectoryPermissionsTest = []Applier{
Apply(
CreateDir("/d1", 0700),
CreateDir("/d1/a", 0700),
CreateDir("/d1/a/b", 0700),
CreateDir("/d1/a/b/c", 0700),
CreateFile("/d1/a/b/f", []byte("content1"), 0644),
CreateDir("/d2", 0751),
CreateDir("/d2/a/b", 0751),
CreateDir("/d2/a/b/c", 0751),
CreateFile("/d2/a/b/f", []byte("content1"), 0644),
),
Apply(
CreateFile("/d1/a/b/f", []byte("content1"), 0644),
Chmod("/d1/a/b/c", 0700),
CreateFile("/d2/a/b/f", []byte("content2"), 0644),
Chmod("/d2/a/b/c", 0751),
),
}
hardlinkUnmodified = []Applier{
baseApplier,
Apply(
CreateFile("/etc/hosts", []byte("127.0.0.1 localhost.localdomain"), 0644),
),
Apply(
Link("/etc/hosts", "/etc/hosts.deny"),
),
}
// Hardlink name before with modification
// Tests link is created for unmodified files when new hardlinked file is seen first
hardlinkBeforeUnmodified = []Applier{
baseApplier,
Apply(
CreateFile("/etc/hosts", []byte("127.0.0.1 localhost.localdomain"), 0644),
),
Apply(
Link("/etc/hosts", "/etc/before-hosts"),
),
}
// Hardlink name after without modification
// tests link is created for modified file with new hardlink
hardlinkBeforeModified = []Applier{
baseApplier,
Apply(
CreateFile("/etc/hosts", []byte("127.0.0.1 localhost.localdomain"), 0644),
),
Apply(
Remove("/etc/hosts"),
CreateFile("/etc/hosts", []byte("127.0.0.1 localhost"), 0644),
Link("/etc/hosts", "/etc/before-hosts"),
),
}
)

43
vendor/github.com/containerd/continuity/fs/hardlink.go generated vendored Normal file
View file

@ -0,0 +1,43 @@
/*
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 fs
import "os"
// GetLinkInfo returns an identifier representing the node a hardlink is pointing
// to. If the file is not hard linked then 0 will be returned.
func GetLinkInfo(fi os.FileInfo) (uint64, bool) {
return getLinkInfo(fi)
}
// getLinkSource returns a path for the given name and
// file info to its link source in the provided inode
// map. If the given file name is not in the map and
// has other links, it is added to the inode map
// to be a source for other link locations.
func getLinkSource(name string, fi os.FileInfo, inodes map[uint64]string) (string, error) {
inode, isHardlink := getLinkInfo(fi)
if !isHardlink {
return "", nil
}
path, ok := inodes[inode]
if !ok {
inodes[inode] = name
}
return path, nil
}

View file

@ -0,0 +1,34 @@
// +build !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 fs
import (
"os"
"syscall"
)
func getLinkInfo(fi os.FileInfo) (uint64, bool) {
s, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return 0, false
}
// Ino is uint32 on bsd, uint64 on darwin/linux/solaris
return uint64(s.Ino), !fi.IsDir() && s.Nlink > 1 // nolint: unconvert
}

View file

@ -0,0 +1,23 @@
/*
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 fs
import "os"
func getLinkInfo(fi os.FileInfo) (uint64, bool) {
return 0, false
}

286
vendor/github.com/containerd/continuity/fs/path.go generated vendored Normal file
View file

@ -0,0 +1,286 @@
/*
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 fs
import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
var (
errTooManyLinks = errors.New("too many links")
)
type currentPath struct {
path string
f os.FileInfo
fullPath string
}
func pathChange(lower, upper *currentPath) (ChangeKind, string) {
if lower == nil {
if upper == nil {
panic("cannot compare nil paths")
}
return ChangeKindAdd, upper.path
}
if upper == nil {
return ChangeKindDelete, lower.path
}
// TODO: compare by directory
switch i := strings.Compare(lower.path, upper.path); {
case i < 0:
// File in lower that is not in upper
return ChangeKindDelete, lower.path
case i > 0:
// File in upper that is not in lower
return ChangeKindAdd, upper.path
default:
return ChangeKindModify, upper.path
}
}
func sameFile(f1, f2 *currentPath) (bool, error) {
if os.SameFile(f1.f, f2.f) {
return true, nil
}
equalStat, err := compareSysStat(f1.f.Sys(), f2.f.Sys())
if err != nil || !equalStat {
return equalStat, err
}
if eq, err := compareCapabilities(f1.fullPath, f2.fullPath); err != nil || !eq {
return eq, err
}
// If not a directory also check size, modtime, and content
if !f1.f.IsDir() {
if f1.f.Size() != f2.f.Size() {
return false, nil
}
t1 := f1.f.ModTime()
t2 := f2.f.ModTime()
if t1.Unix() != t2.Unix() {
return false, nil
}
// If the timestamp may have been truncated in both of the
// files, check content of file to determine difference
if t1.Nanosecond() == 0 && t2.Nanosecond() == 0 {
var eq bool
if (f1.f.Mode() & os.ModeSymlink) == os.ModeSymlink {
eq, err = compareSymlinkTarget(f1.fullPath, f2.fullPath)
} else if f1.f.Size() > 0 {
eq, err = compareFileContent(f1.fullPath, f2.fullPath)
}
if err != nil || !eq {
return eq, err
}
} else if t1.Nanosecond() != t2.Nanosecond() {
return false, nil
}
}
return true, nil
}
func compareSymlinkTarget(p1, p2 string) (bool, error) {
t1, err := os.Readlink(p1)
if err != nil {
return false, err
}
t2, err := os.Readlink(p2)
if err != nil {
return false, err
}
return t1 == t2, nil
}
const compareChuckSize = 32 * 1024
// compareFileContent compares the content of 2 same sized files
// by comparing each byte.
func compareFileContent(p1, p2 string) (bool, error) {
f1, err := os.Open(p1)
if err != nil {
return false, err
}
defer f1.Close()
f2, err := os.Open(p2)
if err != nil {
return false, err
}
defer f2.Close()
b1 := make([]byte, compareChuckSize)
b2 := make([]byte, compareChuckSize)
for {
n1, err1 := f1.Read(b1)
if err1 != nil && err1 != io.EOF {
return false, err1
}
n2, err2 := f2.Read(b2)
if err2 != nil && err2 != io.EOF {
return false, err2
}
if n1 != n2 || !bytes.Equal(b1[:n1], b2[:n2]) {
return false, nil
}
if err1 == io.EOF && err2 == io.EOF {
return true, nil
}
}
}
func pathWalk(ctx context.Context, root string, pathC chan<- *currentPath) error {
return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
// Rebase path
path, err = filepath.Rel(root, path)
if err != nil {
return err
}
path = filepath.Join(string(os.PathSeparator), path)
// Skip root
if path == string(os.PathSeparator) {
return nil
}
p := &currentPath{
path: path,
f: f,
fullPath: filepath.Join(root, path),
}
select {
case <-ctx.Done():
return ctx.Err()
case pathC <- p:
return nil
}
})
}
func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case p := <-pathC:
return p, nil
}
}
// RootPath joins a path with a root, evaluating and bounding any
// symlink to the root directory.
func RootPath(root, path string) (string, error) {
if path == "" {
return root, nil
}
var linksWalked int // to protect against cycles
for {
i := linksWalked
newpath, err := walkLinks(root, path, &linksWalked)
if err != nil {
return "", err
}
path = newpath
if i == linksWalked {
newpath = filepath.Join("/", newpath)
if path == newpath {
return filepath.Join(root, newpath), nil
}
path = newpath
}
}
}
func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) {
if *linksWalked > 255 {
return "", false, errTooManyLinks
}
path = filepath.Join("/", path)
if path == "/" {
return path, false, nil
}
realPath := filepath.Join(root, path)
fi, err := os.Lstat(realPath)
if err != nil {
// If path does not yet exist, treat as non-symlink
if os.IsNotExist(err) {
return path, false, nil
}
return "", false, err
}
if fi.Mode()&os.ModeSymlink == 0 {
return path, false, nil
}
newpath, err = os.Readlink(realPath)
if err != nil {
return "", false, err
}
*linksWalked++
return newpath, true, nil
}
func walkLinks(root, path string, linksWalked *int) (string, error) {
switch dir, file := filepath.Split(path); {
case dir == "":
newpath, _, err := walkLink(root, file, linksWalked)
return newpath, err
case file == "":
if os.IsPathSeparator(dir[len(dir)-1]) {
if dir == "/" {
return dir, nil
}
return walkLinks(root, dir[:len(dir)-1], linksWalked)
}
newpath, _, err := walkLink(root, dir, linksWalked)
return newpath, err
default:
newdir, err := walkLinks(root, dir, linksWalked)
if err != nil {
return "", err
}
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked)
if err != nil {
return "", err
}
if !islink {
return newpath, nil
}
if filepath.IsAbs(newpath) {
return newpath, nil
}
return filepath.Join(newdir, newpath), nil
}
}

310
vendor/github.com/containerd/continuity/fs/path_test.go generated vendored Normal file
View file

@ -0,0 +1,310 @@
// +build !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 fs
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/containerd/continuity/fs/fstest"
"github.com/pkg/errors"
)
type RootCheck struct {
unresolved string
expected string
scope func(string) string
cause error
}
func TestRootPath(t *testing.T) {
tests := []struct {
name string
apply fstest.Applier
checks []RootCheck
}{
{
name: "SymlinkAbsolute",
apply: Symlink("/b", "fs/a/d"),
checks: Check("fs/a/d/c/data", "b/c/data"),
},
{
name: "SymlinkRelativePath",
apply: Symlink("a", "fs/i"),
checks: Check("fs/i", "fs/a"),
},
{
name: "SymlinkSkipSymlinksOutsideScope",
apply: Symlink("realdir", "linkdir"),
checks: CheckWithScope("foo/bar", "foo/bar", "linkdir"),
},
{
name: "SymlinkLastLink",
apply: Symlink("/b", "fs/a/d"),
checks: Check("fs/a/d", "b"),
},
{
name: "SymlinkRelativeLinkChangeScope",
apply: Symlink("../b", "fs/a/e"),
checks: CheckAll(
Check("fs/a/e/c/data", "fs/b/c/data"),
CheckWithScope("e", "b", "fs/a"), // Original return
),
},
{
name: "SymlinkDeepRelativeLinkChangeScope",
apply: Symlink("../../../../test", "fs/a/f"),
checks: CheckAll(
Check("fs/a/f", "test"), // Original return
CheckWithScope("a/f", "test", "fs"), // Original return
),
},
{
name: "SymlinkRelativeLinkChain",
apply: fstest.Apply(
Symlink("../g", "fs/b/h"),
fstest.Symlink("../../../../../../../../../../../../root", "fs/g"),
),
checks: Check("fs/b/h", "root"),
},
{
name: "SymlinkBreakoutPath",
apply: Symlink("../i/a", "fs/j/k"),
checks: CheckWithScope("k", "i/a", "fs/j"),
},
{
name: "SymlinkToRoot",
apply: Symlink("/", "foo"),
checks: Check("foo", ""),
},
{
name: "SymlinkSlashDotdot",
apply: Symlink("/../../", "foo"),
checks: Check("foo", ""),
},
{
name: "SymlinkDotdot",
apply: Symlink("../../", "foo"),
checks: Check("foo", ""),
},
{
name: "SymlinkRelativePath2",
apply: Symlink("baz/target", "bar/foo"),
checks: Check("bar/foo", "bar/baz/target"),
},
{
name: "SymlinkScopeLink",
apply: fstest.Apply(
Symlink("root2", "root"),
Symlink("../bar", "root2/foo"),
),
checks: CheckWithScope("foo", "bar", "root"),
},
{
name: "SymlinkSelf",
apply: fstest.Apply(
Symlink("foo", "root/foo"),
),
checks: ErrorWithScope("foo", "root", errTooManyLinks),
},
{
name: "SymlinkCircular",
apply: fstest.Apply(
Symlink("foo", "bar"),
Symlink("bar", "foo"),
),
checks: ErrorWithScope("foo", "", errTooManyLinks), //TODO: Test for circular error
},
{
name: "SymlinkCircularUnderRoot",
apply: fstest.Apply(
Symlink("baz", "root/bar"),
Symlink("../bak", "root/baz"),
Symlink("/bar", "root/bak"),
),
checks: ErrorWithScope("bar", "root", errTooManyLinks), // TODO: Test for circular error
},
{
name: "SymlinkComplexChain",
apply: fstest.Apply(
fstest.CreateDir("root2", 0777),
Symlink("root2", "root"),
Symlink("r/s", "root/a"),
Symlink("../root/t", "root/r"),
Symlink("/../u", "root/root/t/s/b"),
Symlink(".", "root/u/c"),
Symlink("../v", "root/u/x/y"),
Symlink("/../w", "root/u/v"),
),
checks: CheckWithScope("a/b/c/x/y/z", "w/z", "root"), // Original return
},
{
name: "SymlinkBreakoutNonExistent",
apply: fstest.Apply(
Symlink("/", "root/slash"),
Symlink("/idontexist/../slash", "root/sym"),
),
checks: CheckWithScope("sym/file", "file", "root"),
},
{
name: "SymlinkNoLexicalCleaning",
apply: fstest.Apply(
Symlink("/foo/bar", "root/sym"),
Symlink("/sym/../baz", "root/hello"),
),
checks: CheckWithScope("hello", "foo/baz", "root"),
},
}
for _, test := range tests {
t.Run(test.name, makeRootPathTest(t, test.apply, test.checks))
}
// Add related tests which are unable to follow same pattern
t.Run("SymlinkRootScope", testRootPathSymlinkRootScope)
t.Run("SymlinkEmpty", testRootPathSymlinkEmpty)
}
func testRootPathSymlinkRootScope(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRootScope")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
expected, err := filepath.EvalSymlinks(tmpdir)
if err != nil {
t.Fatal(err)
}
rewrite, err := RootPath("/", tmpdir)
if err != nil {
t.Fatal(err)
}
if rewrite != expected {
t.Fatalf("expected %q got %q", expected, rewrite)
}
}
func testRootPathSymlinkEmpty(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
res, err := RootPath(wd, "")
if err != nil {
t.Fatal(err)
}
if res != wd {
t.Fatalf("expected %q got %q", wd, res)
}
}
func makeRootPathTest(t *testing.T, apply fstest.Applier, checks []RootCheck) func(t *testing.T) {
return func(t *testing.T) {
applyDir, err := ioutil.TempDir("", "test-root-path-")
if err != nil {
t.Fatalf("Unable to make temp directory: %+v", err)
}
defer os.RemoveAll(applyDir)
if apply != nil {
if err := apply.Apply(applyDir); err != nil {
t.Fatalf("Apply failed: %+v", err)
}
}
for i, check := range checks {
root := applyDir
if check.scope != nil {
root = check.scope(root)
}
actual, err := RootPath(root, check.unresolved)
if check.cause != nil {
if err == nil {
t.Errorf("(Check %d) Expected error %q, %q evaluated as %q", i+1, check.cause.Error(), check.unresolved, actual)
}
if errors.Cause(err) != check.cause {
t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
}
} else {
expected := filepath.Join(root, check.expected)
if err != nil {
t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
}
if actual != expected {
t.Errorf("(Check %d) Unexpected evaluated path %q, expected %q", i+1, actual, expected)
}
}
}
}
}
func Check(unresolved, expected string) []RootCheck {
return []RootCheck{
{
unresolved: unresolved,
expected: expected,
},
}
}
func CheckWithScope(unresolved, expected, scope string) []RootCheck {
return []RootCheck{
{
unresolved: unresolved,
expected: expected,
scope: func(root string) string {
return filepath.Join(root, scope)
},
},
}
}
func ErrorWithScope(unresolved, scope string, cause error) []RootCheck {
return []RootCheck{
{
unresolved: unresolved,
cause: cause,
scope: func(root string) string {
return filepath.Join(root, scope)
},
},
}
}
func CheckAll(checks ...[]RootCheck) []RootCheck {
all := make([]RootCheck, 0, len(checks))
for _, c := range checks {
all = append(all, c...)
}
return all
}
func Symlink(oldname, newname string) fstest.Applier {
dir := filepath.Dir(newname)
if dir != "" {
return fstest.Apply(
fstest.CreateDir(dir, 0755),
fstest.Symlink(oldname, newname),
)
}
return fstest.Symlink(oldname, newname)
}

44
vendor/github.com/containerd/continuity/fs/stat_bsd.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
// +build darwin freebsd
/*
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 fs
import (
"syscall"
"time"
)
// StatAtime returns the access time from a stat struct
func StatAtime(st *syscall.Stat_t) syscall.Timespec {
return st.Atimespec
}
// StatCtime returns the created time from a stat struct
func StatCtime(st *syscall.Stat_t) syscall.Timespec {
return st.Ctimespec
}
// StatMtime returns the modified time from a stat struct
func StatMtime(st *syscall.Stat_t) syscall.Timespec {
return st.Mtimespec
}
// StatATimeAsTime returns the access time as a time.Time
func StatATimeAsTime(st *syscall.Stat_t) time.Time {
return time.Unix(int64(st.Atimespec.Sec), int64(st.Atimespec.Nsec)) // nolint: unconvert
}

View file

@ -0,0 +1,43 @@
/*
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 fs
import (
"syscall"
"time"
)
// StatAtime returns the Atim
func StatAtime(st *syscall.Stat_t) syscall.Timespec {
return st.Atim
}
// StatCtime returns the Ctim
func StatCtime(st *syscall.Stat_t) syscall.Timespec {
return st.Ctim
}
// StatMtime returns the Mtim
func StatMtime(st *syscall.Stat_t) syscall.Timespec {
return st.Mtim
}
// StatATimeAsTime returns st.Atim as a time.Time
func StatATimeAsTime(st *syscall.Stat_t) time.Time {
// The int64 conversions ensure the line compiles for 32-bit systems as well.
return time.Unix(int64(st.Atim.Sec), int64(st.Atim.Nsec)) // nolint: unconvert
}

29
vendor/github.com/containerd/continuity/fs/time.go generated vendored Normal file
View file

@ -0,0 +1,29 @@
/*
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 fs
import "time"
// Gnu tar and the go tar writer don't have sub-second mtime
// precision, which is problematic when we apply changes via tar
// files, we handle this by comparing for exact times, *or* same
// second count and either a or b having exactly 0 nanoseconds
func sameFsTime(a, b time.Time) bool {
return a == b ||
(a.Unix() == b.Unix() &&
(a.Nanosecond() == 0 || b.Nanosecond() == 0))
}