godeps update

Signed-off-by: Mike Brown <brownwm@us.ibm.com>
This commit is contained in:
Mike Brown
2017-05-11 19:33:57 -05:00
parent e5199c0cda
commit 100870abb2
224 changed files with 21721 additions and 13 deletions

View File

@@ -0,0 +1,125 @@
package compression
import (
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"sync"
)
type (
// Compression is the state represents if compressed or not.
Compression int
)
const (
// Uncompressed represents the uncompressed.
Uncompressed Compression = iota
// Gzip is gzip compression algorithm.
Gzip
)
var (
bufioReader32KPool = &sync.Pool{
New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) },
}
)
type readCloserWrapper struct {
io.Reader
closer func() error
}
func (r *readCloserWrapper) Close() error {
if r.closer != nil {
return r.closer()
}
return nil
}
type writeCloserWrapper struct {
io.Writer
closer func() error
}
func (w *writeCloserWrapper) Close() error {
if w.closer != nil {
w.closer()
}
return nil
}
// DetectCompression detects the compression algorithm of the source.
func DetectCompression(source []byte) Compression {
for compression, m := range map[Compression][]byte{
Gzip: {0x1F, 0x8B, 0x08},
} {
if len(source) < len(m) {
// Len too short
continue
}
if bytes.Equal(m, source[:len(m)]) {
return compression
}
}
return Uncompressed
}
// DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive.
func DecompressStream(archive io.Reader) (io.ReadCloser, error) {
buf := bufioReader32KPool.Get().(*bufio.Reader)
buf.Reset(archive)
bs, err := buf.Peek(10)
if err != nil && err != io.EOF {
// Note: we'll ignore any io.EOF error because there are some odd
// cases where the layer.tar file will be empty (zero bytes) and
// that results in an io.EOF from the Peek() call. So, in those
// cases we'll just treat it as a non-compressed stream and
// that means just create an empty layer.
// See Issue docker/docker#18170
return nil, err
}
closer := func() error {
buf.Reset(nil)
bufioReader32KPool.Put(buf)
return nil
}
switch compression := DetectCompression(bs); compression {
case Uncompressed:
readBufWrapper := &readCloserWrapper{buf, closer}
return readBufWrapper, nil
case Gzip:
gzReader, err := gzip.NewReader(buf)
if err != nil {
return nil, err
}
readBufWrapper := &readCloserWrapper{gzReader, closer}
return readBufWrapper, nil
default:
return nil, fmt.Errorf("unsupported compression format %s", (&compression).Extension())
}
}
// CompressStream compresseses the dest with specified compression algorithm.
func CompressStream(dest io.Writer, compression Compression) (io.WriteCloser, error) {
switch compression {
case Uncompressed:
return &writeCloserWrapper{dest, nil}, nil
case Gzip:
return gzip.NewWriter(dest), nil
default:
return nil, fmt.Errorf("unsupported compression format %s", (&compression).Extension())
}
}
// Extension returns the extension of a file that uses the specified compression algorithm.
func (compression *Compression) Extension() string {
switch *compression {
case Gzip:
return "gz"
}
return ""
}

490
vendor/github.com/containerd/containerd/archive/tar.go generated vendored Normal file
View File

@@ -0,0 +1,490 @@
package archive
import (
"archive/tar"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/containerd/containerd/fs"
"github.com/containerd/containerd/log"
"github.com/pkg/errors"
)
var (
bufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, 32*1024)
},
}
breakoutError = errors.New("file name outside of root")
)
// Diff returns a tar stream of the computed filesystem
// difference between the provided directories.
//
// Produces a tar using OCI style file markers for deletions. Deleted
// files will be prepended with the prefix ".wh.". This style is
// based off AUFS whiteouts.
// See https://github.com/opencontainers/image-spec/blob/master/layer.md
func Diff(ctx context.Context, a, b string) io.ReadCloser {
r, w := io.Pipe()
go func() {
var err error
cw := newChangeWriter(w, b)
if err = fs.Changes(ctx, a, b, cw.HandleChange); err != nil {
err = errors.Wrap(err, "failed to create diff tar stream")
} else {
err = cw.Close()
}
if err = w.CloseWithError(err); err != nil {
log.G(ctx).WithError(err).Debugf("closing tar pipe failed")
}
}()
return r
}
const (
// whiteoutPrefix prefix means file is a whiteout. If this is followed by a
// filename this means that file has been removed from the base layer.
// See https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts
whiteoutPrefix = ".wh."
// whiteoutMetaPrefix prefix means whiteout has a special meaning and is not
// for removing an actual file. Normally these files are excluded from exported
// 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"
)
// 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, r io.Reader) (int64, error) {
root = filepath.Clean(root)
fn := prepareApply()
defer fn()
var (
tr = tar.NewReader(r)
size int64
dirs []*tar.Header
// Used for handling opaque directory markers which
// may occur out of order
unpackedPaths = make(map[string]struct{})
// Used for aufs plink directory
aufsTempdir = ""
aufsHardlinks = make(map[string]*tar.Header)
)
// Iterate through the files in the archive.
for {
hdr, err := tr.Next()
if err == io.EOF {
// end of tar archive
break
}
if err != nil {
return 0, err
}
size += hdr.Size
// Normalize name, for safety and for a simple is-root check
hdr.Name = filepath.Clean(hdr.Name)
if skipFile(hdr) {
log.G(ctx).Warnf("file %q ignored: archive may not be supported on system", hdr.Name)
continue
}
// Note as these operations are platform specific, so must the slash be.
if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) {
// Not the root directory, ensure that the parent directory exists.
// This happened in some tests where an image had a tarfile without any
// parent directories.
parent := filepath.Dir(hdr.Name)
parentPath := filepath.Join(root, parent)
if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) {
err = mkdirAll(parentPath, 0600)
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("", "dockerplnk"); err != nil {
return 0, err
}
defer os.RemoveAll(aufsTempdir)
}
if err := createTarFile(ctx, filepath.Join(aufsTempdir, basename), root, hdr, tr); err != nil {
return 0, err
}
}
if hdr.Name != whiteoutOpaqueDir {
continue
}
}
path := filepath.Join(root, hdr.Name)
rel, err := filepath.Rel(root, path)
if err != nil {
return 0, err
}
// Note as these operations are platform specific, so must the slash be.
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return 0, errors.Wrapf(breakoutError, "%q is outside of %q", hdr.Name, root)
}
base := filepath.Base(path)
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)
if err := os.RemoveAll(originalPath); err != nil {
return 0, err
}
continue
}
// If path exits we almost always just want to remove and replace it.
// The only exception is when it is a directory *and* the file from
// the layer is also a directory. Then we want to merge them (i.e.
// just apply the metadata from the layer).
if fi, err := os.Lstat(path); err == nil {
if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) {
if err := os.RemoveAll(path); err != nil {
return 0, err
}
}
}
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")
}
tmpFile, err := os.Open(filepath.Join(aufsTempdir, linkBasename))
if err != nil {
return 0, err
}
defer tmpFile.Close()
srcData = tmpFile
}
if err := createTarFile(ctx, path, root, srcHdr, srcData); err != nil {
return 0, err
}
// Directory mtimes must be handled at the end to avoid further
// file creation in them to modify the directory mtime
if hdr.Typeflag == tar.TypeDir {
dirs = append(dirs, hdr)
}
unpackedPaths[path] = struct{}{}
}
for _, hdr := range dirs {
path := filepath.Join(root, hdr.Name)
if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil {
return 0, err
}
}
return size, nil
}
type changeWriter struct {
tw *tar.Writer
source string
whiteoutT time.Time
inodeCache map[uint64]string
}
func newChangeWriter(w io.Writer, source string) *changeWriter {
return &changeWriter{
tw: tar.NewWriter(w),
source: source,
whiteoutT: time.Now(),
inodeCache: map[uint64]string{},
}
}
func (cw *changeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if k == fs.ChangeKindDelete {
whiteOutDir := filepath.Dir(p)
whiteOutBase := filepath.Base(p)
whiteOut := filepath.Join(whiteOutDir, whiteoutPrefix+whiteOutBase)
hdr := &tar.Header{
Name: whiteOut[1:],
Size: 0,
ModTime: cw.whiteoutT,
AccessTime: cw.whiteoutT,
ChangeTime: cw.whiteoutT,
}
if err := cw.tw.WriteHeader(hdr); err != nil {
errors.Wrap(err, "failed to write whiteout header")
}
} else {
var (
link string
err error
source = filepath.Join(cw.source, p)
)
if f.Mode()&os.ModeSymlink != 0 {
if link, err = os.Readlink(source); err != nil {
return err
}
}
hdr, err := tar.FileInfoHeader(f, link)
if err != nil {
return err
}
hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
name := p
if strings.HasPrefix(name, string(filepath.Separator)) {
name, err = filepath.Rel(string(filepath.Separator), name)
if err != nil {
return errors.Wrap(err, "failed to make path relative")
}
}
name, err = tarName(name)
if err != nil {
return errors.Wrap(err, "cannot canonicalize path")
}
// suffix with '/' for directories
if f.IsDir() && !strings.HasSuffix(name, "/") {
name += "/"
}
hdr.Name = name
if err := setHeaderForSpecialDevice(hdr, name, f); err != nil {
return errors.Wrap(err, "failed to set device headers")
}
linkname, err := fs.GetLinkSource(name, f, cw.inodeCache)
if err != nil {
return errors.Wrap(err, "failed to get hardlink")
}
if linkname != "" {
hdr.Typeflag = tar.TypeLink
hdr.Linkname = linkname
hdr.Size = 0
}
if capability, err := getxattr(source, "security.capability"); err != nil {
return errors.Wrap(err, "failed to get capabilities xattr")
} else if capability != nil {
hdr.Xattrs = map[string]string{
"security.capability": string(capability),
}
}
if err := cw.tw.WriteHeader(hdr); err != nil {
return errors.Wrap(err, "failed to write file header")
}
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
file, err := open(source)
if err != nil {
return errors.Wrapf(err, "failed to open path: %v", source)
}
defer file.Close()
buf := bufferPool.Get().([]byte)
n, err := io.CopyBuffer(cw.tw, file, buf)
bufferPool.Put(buf)
if err != nil {
return errors.Wrap(err, "failed to copy")
}
if n != hdr.Size {
return errors.New("short write copying file")
}
}
}
return nil
}
func (cw *changeWriter) Close() error {
if err := cw.tw.Close(); err != nil {
return errors.Wrap(err, "failed to close tar writer")
}
return nil
}
func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader) error {
// hdr.Mode is in linux format, which we can use for syscalls,
// but for os.Foo() calls we need the mode converted to os.FileMode,
// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
hdrInfo := hdr.FileInfo()
switch hdr.Typeflag {
case tar.TypeDir:
// Create directory unless it exists as a directory already.
// In that case we just want to merge the two
if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) {
if err := os.Mkdir(path, hdrInfo.Mode()); err != nil {
return err
}
}
case tar.TypeReg, tar.TypeRegA:
file, err := openFile(path, os.O_CREATE|os.O_WRONLY, hdrInfo.Mode())
if err != nil {
return err
}
buf := bufferPool.Get().([]byte)
_, err = io.CopyBuffer(file, reader, buf)
if err1 := file.Close(); err == nil {
err = err1
}
if err != nil {
return err
}
case tar.TypeBlock, tar.TypeChar:
// Handle this is an OS-specific way
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
return err
}
case tar.TypeFifo:
// Handle this is an OS-specific way
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
return err
}
case tar.TypeLink:
targetPath := filepath.Join(extractDir, hdr.Linkname)
// check for hardlink breakout
if !strings.HasPrefix(targetPath, extractDir) {
return errors.Wrapf(breakoutError, "invalid hardlink %q -> %q", targetPath, hdr.Linkname)
}
if err := os.Link(targetPath, path); err != nil {
return err
}
case tar.TypeSymlink:
// path -> hdr.Linkname = targetPath
// e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file
targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname)
// the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because
// that symlink would first have to be created, which would be caught earlier, at this very check:
if !strings.HasPrefix(targetPath, extractDir) {
return errors.Wrapf(breakoutError, "invalid symlink %q -> %q", path, hdr.Linkname)
}
if err := os.Symlink(hdr.Linkname, path); err != nil {
return err
}
case tar.TypeXGlobalHeader:
log.G(ctx).Debug("PAX Global Extended Headers found and ignored")
return nil
default:
return errors.Errorf("unhandled tar header type %d\n", hdr.Typeflag)
}
// Lchown is not supported on Windows.
if runtime.GOOS != "windows" {
if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil {
return err
}
}
for key, value := range hdr.Xattrs {
if err := setxattr(path, key, value); err != nil {
if errors.Cause(err) == syscall.ENOTSUP {
log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key)
continue
}
return err
}
}
// There is no LChmod, so ignore mode for symlink. Also, this
// must happen after chown, as that can modify the file mode
if err := handleLChmod(hdr, path, hdrInfo); err != nil {
return err
}
if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,131 @@
package archive
import (
"archive/tar"
"os"
"sync"
"syscall"
"github.com/opencontainers/runc/libcontainer/system"
"github.com/pkg/errors"
"github.com/stevvooe/continuity/sysx"
)
func tarName(p string) (string, error) {
return p, nil
}
func chmodTarEntry(perm os.FileMode) os.FileMode {
return perm
}
func setHeaderForSpecialDevice(hdr *tar.Header, name string, fi os.FileInfo) error {
s, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("unsupported stat type")
}
// Currently go does not fill in the major/minors
if s.Mode&syscall.S_IFBLK != 0 ||
s.Mode&syscall.S_IFCHR != 0 {
hdr.Devmajor = int64(major(uint64(s.Rdev)))
hdr.Devminor = int64(minor(uint64(s.Rdev)))
}
return nil
}
func major(device uint64) uint64 {
return (device >> 8) & 0xfff
}
func minor(device uint64) uint64 {
return (device & 0xff) | ((device >> 12) & 0xfff00)
}
func mkdev(major int64, minor int64) uint32 {
return uint32(((minor & 0xfff00) << 12) | ((major & 0xfff) << 8) | (minor & 0xff))
}
func open(p string) (*os.File, error) {
return os.Open(p)
}
func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
return os.OpenFile(name, flag, perm)
}
func mkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
func prepareApply() func() {
// Unset unmask before doing an apply operation,
// restore unmask when complete
oldmask := syscall.Umask(0)
return func() {
syscall.Umask(oldmask)
}
}
func skipFile(*tar.Header) bool {
return false
}
var (
inUserNS bool
nsOnce sync.Once
)
func setInUserNS() {
inUserNS = system.RunningInUserNS()
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
nsOnce.Do(setInUserNS)
if inUserNS {
// cannot create a device if running in user namespace
return nil
}
mode := uint32(hdr.Mode & 07777)
switch hdr.Typeflag {
case tar.TypeBlock:
mode |= syscall.S_IFBLK
case tar.TypeChar:
mode |= syscall.S_IFCHR
case tar.TypeFifo:
mode |= syscall.S_IFIFO
}
return syscall.Mknod(path, mode, int(mkdev(hdr.Devmajor, hdr.Devminor)))
}
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
if hdr.Typeflag == tar.TypeLink {
if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
return err
}
}
} else if hdr.Typeflag != tar.TypeSymlink {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
return err
}
}
return nil
}
func getxattr(path, attr string) ([]byte, error) {
b, err := sysx.LGetxattr(path, attr)
if err == syscall.ENOTSUP || err == syscall.ENODATA {
return nil, nil
}
return b, err
}
func setxattr(path, key, value string) error {
return sysx.LSetxattr(path, key, []byte(value), 0)
}

View File

@@ -0,0 +1,104 @@
package archive
import (
"archive/tar"
"errors"
"fmt"
"os"
"strings"
"github.com/containerd/containerd/sys"
)
// tarName returns platform-specific filepath
// to canonical posix-style path for tar archival. p is relative
// path.
func tarName(p string) (string, error) {
// windows: convert windows style relative path with backslashes
// into forward slashes. Since windows does not allow '/' or '\'
// in file names, it is mostly safe to replace however we must
// check just in case
if strings.Contains(p, "/") {
return "", fmt.Errorf("Windows path contains forward slash: %s", p)
}
return strings.Replace(p, string(os.PathSeparator), "/", -1), nil
}
// chmodTarEntry is used to adjust the file permissions used in tar header based
// on the platform the archival is done.
func chmodTarEntry(perm os.FileMode) os.FileMode {
perm &= 0755
// Add the x bit: make everything +x from windows
perm |= 0111
return perm
}
func setHeaderForSpecialDevice(*tar.Header, string, os.FileInfo) error {
// do nothing. no notion of Rdev, Inode, Nlink in stat on Windows
return nil
}
func open(p string) (*os.File, error) {
// We use sys.OpenSequential to ensure we use sequential file
// access on Windows to avoid depleting the standby list.
return sys.OpenSequential(p)
}
func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
// Source is regular file. We use sys.OpenFileSequential to use sequential
// file access to avoid depleting the standby list on Windows.
return sys.OpenFileSequential(name, flag, perm)
}
func mkdirAll(path string, perm os.FileMode) error {
return sys.MkdirAll(path, perm)
}
func prepareApply() func() {
// No umask or filesystem changes needed before apply
return func() {}
}
func skipFile(hdr *tar.Header) bool {
// Windows does not support filenames with colons in them. Ignore
// these files. This is not a problem though (although it might
// appear that it is). Let's suppose a client is running docker pull.
// The daemon it points to is Windows. Would it make sense for the
// client to be doing a docker pull Ubuntu for example (which has files
// with colons in the name under /usr/share/man/man3)? No, absolutely
// not as it would really only make sense that they were pulling a
// Windows image. However, for development, it is necessary to be able
// to pull Linux images which are in the repository.
//
// TODO Windows. Once the registry is aware of what images are Windows-
// specific or Linux-specific, this warning should be changed to an error
// to cater for the situation where someone does manage to upload a Linux
// image but have it tagged as Windows inadvertently.
if strings.Contains(hdr.Name, ":") {
return true
}
return false
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
return nil
}
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
return nil
}
func getxattr(path, attr string) ([]byte, error) {
return nil, nil
}
func setxattr(path, key, value string) error {
// Return not support error, do not wrap underlying not supported
// since xattrs should not exist in windows diff archives
return errors.New("xattrs not supported on Windows")
}

View File

@@ -0,0 +1,38 @@
package archive
import (
"syscall"
"time"
"unsafe"
)
var (
minTime = time.Unix(0, 0)
maxTime time.Time
)
func init() {
if unsafe.Sizeof(syscall.Timespec{}.Nsec) == 8 {
// This is a 64 bit timespec
// os.Chtimes limits time to the following
maxTime = time.Unix(0, 1<<63-1)
} else {
// This is a 32 bit timespec
maxTime = time.Unix(1<<31-1, 0)
}
}
func boundTime(t time.Time) time.Time {
if t.Before(minTime) || t.After(maxTime) {
return minTime
}
return t
}
func latestTime(t1, t2 time.Time) time.Time {
if t1.Before(t2) {
return t2
}
return t1
}

View File

@@ -0,0 +1,21 @@
package archive
import (
"time"
"golang.org/x/sys/unix"
"github.com/pkg/errors"
)
func chtimes(path string, atime, mtime time.Time) error {
var utimes [2]unix.Timespec
utimes[0] = unix.NsecToTimespec(atime.UnixNano())
utimes[1] = unix.NsecToTimespec(mtime.UnixNano())
if err := unix.UtimesNanoAt(unix.AT_FDCWD, path, utimes[0:], unix.AT_SYMLINK_NOFOLLOW); err != nil {
return errors.Wrap(err, "failed call to UtimesNanoAt")
}
return nil
}

View File

@@ -0,0 +1,25 @@
package archive
import (
"syscall"
"time"
)
// chtimes will set the create time on a file using the given modtime.
// This requires calling SetFileTime and explicitly including the create time.
func chtimes(path string, atime, mtime time.Time) error {
ctimespec := syscall.NsecToTimespec(mtime.UnixNano())
pathp, e := syscall.UTF16PtrFromString(path)
if e != nil {
return e
}
h, e := syscall.CreateFile(pathp,
syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil,
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if e != nil {
return e
}
defer syscall.Close(h)
c := syscall.NsecToFiletime(syscall.TimespecToNsec(ctimespec))
return syscall.SetFileTime(h, &c, nil, nil)
}

View File

@@ -0,0 +1,67 @@
package content
import (
"context"
"io"
"sync"
"time"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
var (
// ErrNotFound is returned when an item is not found.
//
// Use IsNotFound(err) to detect this condition.
ErrNotFound = errors.New("content: not found")
// ErrExists is returned when something exists when it may not be expected.
//
// Use IsExists(err) to detect this condition.
ErrExists = errors.New("content: exists")
bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, 1<<20)
},
}
)
type Info struct {
Digest digest.Digest
Size int64
CommittedAt time.Time
}
type Provider interface {
Reader(ctx context.Context, dgst digest.Digest) (io.ReadCloser, error)
}
type Status struct {
Ref string
Offset int64
Total int64
StartedAt time.Time
UpdatedAt time.Time
}
type Writer interface {
io.WriteCloser
Status() (Status, error)
Digest() digest.Digest
Commit(size int64, expected digest.Digest) error
Truncate(size int64) error
}
type Ingester interface {
Writer(ctx context.Context, ref string, size int64, expected digest.Digest) (Writer, error)
}
func IsNotFound(err error) bool {
return errors.Cause(err) == ErrNotFound
}
func IsExists(err error) bool {
return errors.Cause(err) == ErrExists
}

View File

@@ -0,0 +1,116 @@
package content
import (
"context"
"fmt"
"io"
"io/ioutil"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// ReadBlob retrieves the entire contents of the blob from the provider.
//
// Avoid using this for large blobs, such as layers.
func ReadBlob(ctx context.Context, provider Provider, dgst digest.Digest) ([]byte, error) {
rc, err := provider.Reader(ctx, dgst)
if err != nil {
return nil, err
}
defer rc.Close()
return ioutil.ReadAll(rc)
}
// WriteBlob writes data with the expected digest into the content store. If
// expected already exists, the method returns immediately and the reader will
// not be consumed.
//
// This is useful when the digest and size are known beforehand.
//
// Copy is buffered, so no need to wrap reader in buffered io.
func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size int64, expected digest.Digest) error {
cw, err := cs.Writer(ctx, ref, size, expected)
if err != nil {
if !IsExists(err) {
return err
}
return nil // all ready present
}
defer cw.Close()
ws, err := cw.Status()
if err != nil {
return err
}
if ws.Offset > 0 {
r, err = seekReader(r, ws.Offset, size)
if err != nil {
if !isUnseekable(err) {
return errors.Wrapf(err, "unabled to resume write to %v", ref)
}
// reader is unseekable, try to move the writer back to the start.
if err := cw.Truncate(0); err != nil {
return errors.Wrapf(err, "content writer truncate failed")
}
}
}
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf)
if _, err := io.CopyBuffer(cw, r, buf); err != nil {
return err
}
if err := cw.Commit(size, expected); err != nil {
if !IsExists(err) {
return errors.Wrapf(err, "failed commit on ref %q", ref)
}
}
return nil
}
var errUnseekable = errors.New("seek not supported")
func isUnseekable(err error) bool {
return errors.Cause(err) == errUnseekable
}
// seekReader attempts to seek the reader to the given offset, either by
// resolving `io.Seeker` or by detecting `io.ReaderAt`.
func seekReader(r io.Reader, offset, size int64) (io.Reader, error) {
// attempt to resolve r as a seeker and setup the offset.
seeker, ok := r.(io.Seeker)
if ok {
nn, err := seeker.Seek(offset, io.SeekStart)
if nn != offset {
return nil, fmt.Errorf("failed to seek to offset %v", offset)
}
if err != nil {
return nil, err
}
return r, nil
}
// ok, let's try io.ReaderAt!
readerAt, ok := r.(io.ReaderAt)
if ok && size > offset {
sr := io.NewSectionReader(readerAt, offset, size)
return sr, nil
}
return r, errors.Wrapf(errUnseekable, "seek to offset %v failed", offset)
}
func readFileString(path string) (string, error) {
p, err := ioutil.ReadFile(path)
return string(p), err
}

View File

@@ -0,0 +1,56 @@
package content
import (
"sync"
"github.com/nightlyone/lockfile"
"github.com/pkg/errors"
)
// In addition to providing inter-process locks for content ingest, we also
// define a global in process lock to prevent two goroutines writing to the
// same file.
//
// This is pretty unsophisticated for now. In the future, we'd probably like to
// have more information about who is holding which locks, as well as better
// error reporting.
var (
errLocked = errors.New("key is locked")
// locks lets us lock in process, as well as output of process.
locks = map[lockfile.Lockfile]struct{}{}
locksMu sync.Mutex
)
func tryLock(lock lockfile.Lockfile) error {
locksMu.Lock()
defer locksMu.Unlock()
if _, ok := locks[lock]; ok {
return errLocked
}
if err := lock.TryLock(); err != nil {
if errors.Cause(err) == lockfile.ErrBusy {
return errLocked
}
return errors.Wrapf(err, "lock.TryLock() encountered an error")
}
locks[lock] = struct{}{}
return nil
}
func unlock(lock lockfile.Lockfile) error {
locksMu.Lock()
defer locksMu.Unlock()
if _, ok := locks[lock]; !ok {
return nil
}
delete(locks, lock)
return lock.Unlock()
}

View File

@@ -0,0 +1,368 @@
package content
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"
"github.com/containerd/containerd/log"
"github.com/nightlyone/lockfile"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// Store is digest-keyed store for content. All data written into the store is
// stored under a verifiable digest.
//
// Store can generally support multi-reader, single-writer ingest of data,
// including resumable ingest.
type Store struct {
root string
}
func NewStore(root string) (*Store, error) {
if err := os.MkdirAll(filepath.Join(root, "ingest"), 0777); err != nil && !os.IsExist(err) {
return nil, err
}
return &Store{
root: root,
}, nil
}
func (s *Store) Info(dgst digest.Digest) (Info, error) {
p := s.blobPath(dgst)
fi, err := os.Stat(p)
if err != nil {
if os.IsNotExist(err) {
err = ErrNotFound
}
return Info{}, err
}
return Info{
Digest: dgst,
Size: fi.Size(),
CommittedAt: fi.ModTime(),
}, nil
}
// Open returns an io.ReadCloser for the blob.
//
// TODO(stevvooe): This would work much better as an io.ReaderAt in practice.
// Right now, we are doing type assertion to tease that out, but it won't scale
// well.
func (s *Store) Reader(ctx context.Context, dgst digest.Digest) (io.ReadCloser, error) {
fp, err := os.Open(s.blobPath(dgst))
if err != nil {
if os.IsNotExist(err) {
err = ErrNotFound
}
return nil, err
}
return fp, nil
}
// Delete removes a blob by its digest.
//
// While this is safe to do concurrently, safe exist-removal logic must hold
// some global lock on the store.
func (cs *Store) Delete(dgst digest.Digest) error {
if err := os.RemoveAll(cs.blobPath(dgst)); err != nil {
if !os.IsNotExist(err) {
return err
}
return ErrNotFound
}
return nil
}
// TODO(stevvooe): Allow querying the set of blobs in the blob store.
// WalkFunc defines the callback for a blob walk.
//
// TODO(stevvooe): Remove the file info. Just need size and modtime. Perhaps,
// not a huge deal, considering we have a path, but let's not just let this one
// go without scrutiny.
type WalkFunc func(path string, fi os.FileInfo, dgst digest.Digest) error
func (cs *Store) Walk(fn WalkFunc) error {
root := filepath.Join(cs.root, "blobs")
var alg digest.Algorithm
return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.IsDir() && !alg.Available() {
return nil
}
// TODO(stevvooe): There are few more cases with subdirs that should be
// handled in case the layout gets corrupted. This isn't strict enough
// an may spew bad data.
if path == root {
return nil
}
if filepath.Dir(path) == root {
alg = digest.Algorithm(filepath.Base(path))
if !alg.Available() {
alg = ""
return filepath.SkipDir
}
// descending into a hash directory
return nil
}
dgst := digest.NewDigestFromHex(alg.String(), filepath.Base(path))
if err := dgst.Validate(); err != nil {
// log error but don't report
log.L.WithError(err).WithField("path", path).Error("invalid digest for blob path")
// if we see this, it could mean some sort of corruption of the
// store or extra paths not expected previously.
}
return fn(path, fi, dgst)
})
}
// Status returns the current status of a blob by the ingest ref.
func (s *Store) Status(ref string) (Status, error) {
return s.status(s.ingestRoot(ref))
}
// status works like stat above except uses the path to the ingest.
func (s *Store) status(ingestPath string) (Status, error) {
dp := filepath.Join(ingestPath, "data")
fi, err := os.Stat(dp)
if err != nil {
return Status{}, err
}
ref, err := readFileString(filepath.Join(ingestPath, "ref"))
if err != nil {
return Status{}, err
}
return Status{
Ref: ref,
Offset: fi.Size(),
Total: s.total(ingestPath),
UpdatedAt: fi.ModTime(),
StartedAt: getStartTime(fi),
}, nil
}
// total attempts to resolve the total expected size for the write.
func (s *Store) total(ingestPath string) int64 {
totalS, err := readFileString(filepath.Join(ingestPath, "total"))
if err != nil {
return 0
}
total, err := strconv.ParseInt(totalS, 10, 64)
if err != nil {
// represents a corrupted file, should probably remove.
return 0
}
return total
}
// Writer begins or resumes the active writer identified by ref. If the writer
// is already in use, an error is returned. Only one writer may be in use per
// ref at a time.
//
// The argument `ref` is used to uniquely identify a long-lived writer transaction.
func (s *Store) Writer(ctx context.Context, ref string, total int64, expected digest.Digest) (Writer, error) {
path, refp, data, lock, err := s.ingestPaths(ref)
if err != nil {
return nil, err
}
if err := tryLock(lock); err != nil {
if !os.IsNotExist(errors.Cause(err)) {
return nil, errors.Wrapf(err, "locking %v failed", ref)
}
// if it doesn't exist, we'll make it so below!
}
var (
digester = digest.Canonical.Digester()
offset int64
startedAt time.Time
updatedAt time.Time
)
// ensure that the ingest path has been created.
if err := os.Mkdir(path, 0755); err != nil {
if !os.IsExist(err) {
return nil, err
}
status, err := s.status(path)
if err != nil {
return nil, errors.Wrap(err, "failed reading status of resume write")
}
if ref != status.Ref {
// NOTE(stevvooe): This is fairly catastrophic. Either we have some
// layout corruption or a hash collision for the ref key.
return nil, errors.Wrapf(err, "ref key does not match: %v != %v", ref, status.Ref)
}
if total > 0 && status.Total > 0 && total != status.Total {
return nil, errors.Errorf("provided total differs from status: %v != %v", total, status.Total)
}
// slow slow slow!!, send to goroutine or use resumable hashes
fp, err := os.Open(data)
if err != nil {
return nil, err
}
defer fp.Close()
p := bufPool.Get().([]byte)
defer bufPool.Put(p)
offset, err = io.CopyBuffer(digester.Hash(), fp, p)
if err != nil {
return nil, err
}
updatedAt = status.UpdatedAt
startedAt = status.StartedAt
total = status.Total
} else {
// the ingest is new, we need to setup the target location.
// write the ref to a file for later use
if err := ioutil.WriteFile(refp, []byte(ref), 0666); err != nil {
return nil, err
}
if total > 0 {
if err := ioutil.WriteFile(filepath.Join(path, "total"), []byte(fmt.Sprint(total)), 0666); err != nil {
return nil, err
}
}
startedAt = time.Now()
updatedAt = startedAt
}
fp, err := os.OpenFile(data, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return nil, errors.Wrap(err, "failed to open data file")
}
return &writer{
s: s,
fp: fp,
lock: lock,
ref: ref,
path: path,
offset: offset,
total: total,
digester: digester,
startedAt: startedAt,
updatedAt: updatedAt,
}, nil
}
// Abort an active transaction keyed by ref. If the ingest is active, it will
// be cancelled. Any resources associated with the ingest will be cleaned.
func (s *Store) Abort(ref string) error {
root := s.ingestRoot(ref)
if err := os.RemoveAll(root); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
return nil
}
func (s *Store) Active() ([]Status, error) {
fp, err := os.Open(filepath.Join(s.root, "ingest"))
if err != nil {
return nil, err
}
defer fp.Close()
fis, err := fp.Readdir(-1)
if err != nil {
return nil, err
}
var active []Status
for _, fi := range fis {
p := filepath.Join(s.root, "ingest", fi.Name())
stat, err := s.status(p)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
// TODO(stevvooe): This is a common error if uploads are being
// completed while making this listing. Need to consider taking a
// lock on the whole store to coordinate this aspect.
//
// Another option is to cleanup downloads asynchronously and
// coordinate this method with the cleanup process.
//
// For now, we just skip them, as they really don't exist.
continue
}
active = append(active, stat)
}
return active, nil
}
func (cs *Store) blobPath(dgst digest.Digest) string {
return filepath.Join(cs.root, "blobs", dgst.Algorithm().String(), dgst.Hex())
}
func (s *Store) ingestRoot(ref string) string {
dgst := digest.FromString(ref)
return filepath.Join(s.root, "ingest", dgst.Hex())
}
// ingestPaths are returned, including the lockfile. The paths are the following:
//
// - root: entire ingest directory
// - ref: name of the starting ref, must be unique
// - data: file where data is written
// - lock: lock file location
//
func (s *Store) ingestPaths(ref string) (string, string, string, lockfile.Lockfile, error) {
var (
fp = s.ingestRoot(ref)
rp = filepath.Join(fp, "ref")
lp = filepath.Join(fp, "lock")
dp = filepath.Join(fp, "data")
)
lock, err := lockfile.New(lp)
if err != nil {
return "", "", "", "", errors.Wrapf(err, "error creating lockfile %v", lp)
}
return fp, rp, dp, lock, nil
}

View File

@@ -0,0 +1,17 @@
// +build linux
package content
import (
"os"
"syscall"
"time"
)
func getStartTime(fi os.FileInfo) time.Time {
if st, ok := fi.Sys().(*syscall.Stat_t); ok {
return time.Unix(st.Ctim.Sec, st.Ctim.Nsec)
}
return fi.ModTime()
}

View File

@@ -0,0 +1,10 @@
package content
import (
"os"
"time"
)
func getStartTime(fi os.FileInfo) time.Time {
return fi.ModTime()
}

View File

@@ -0,0 +1,144 @@
package content
import (
"os"
"path/filepath"
"time"
"github.com/containerd/containerd/log"
"github.com/nightlyone/lockfile"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// writer represents a write transaction against the blob store.
type writer struct {
s *Store
fp *os.File // opened data file
lock lockfile.Lockfile
path string // path to writer dir
ref string // ref key
offset int64
total int64
digester digest.Digester
startedAt time.Time
updatedAt time.Time
}
func (w *writer) Status() (Status, error) {
return Status{
Ref: w.ref,
Offset: w.offset,
Total: w.total,
StartedAt: w.startedAt,
UpdatedAt: w.updatedAt,
}, nil
}
// Digest returns the current digest of the content, up to the current write.
//
// Cannot be called concurrently with `Write`.
func (w *writer) Digest() digest.Digest {
return w.digester.Digest()
}
// Write p to the transaction.
//
// Note that writes are unbuffered to the backing file. When writing, it is
// recommended to wrap in a bufio.Writer or, preferably, use io.CopyBuffer.
func (w *writer) Write(p []byte) (n int, err error) {
n, err = w.fp.Write(p)
w.digester.Hash().Write(p[:n])
w.offset += int64(len(p))
w.updatedAt = time.Now()
return n, err
}
func (w *writer) Commit(size int64, expected digest.Digest) error {
if err := w.fp.Sync(); err != nil {
return errors.Wrap(err, "sync failed")
}
fi, err := w.fp.Stat()
if err != nil {
return errors.Wrap(err, "stat on ingest file failed")
}
// change to readonly, more important for read, but provides _some_
// protection from this point on. We use the existing perms with a mask
// only allowing reads honoring the umask on creation.
//
// This removes write and exec, only allowing read per the creation umask.
if err := w.fp.Chmod((fi.Mode() & os.ModePerm) &^ 0333); err != nil {
return errors.Wrap(err, "failed to change ingest file permissions")
}
if size > 0 && size != fi.Size() {
return errors.Errorf("%q failed size validation: %v != %v", w.ref, fi.Size(), size)
}
if err := w.fp.Close(); err != nil {
return errors.Wrap(err, "failed closing ingest")
}
dgst := w.digester.Digest()
if expected != "" && expected != dgst {
return errors.Errorf("unexpected digest: %v != %v", dgst, expected)
}
var (
ingest = filepath.Join(w.path, "data")
target = w.s.blobPath(dgst)
)
// make sure parent directories of blob exist
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
// clean up!!
defer os.RemoveAll(w.path)
if err := os.Rename(ingest, target); err != nil {
if os.IsExist(err) {
// collision with the target file!
return ErrExists
}
return err
}
unlock(w.lock)
w.fp = nil
return nil
}
// Close the writer, flushing any unwritten data and leaving the progress in
// tact.
//
// If one needs to resume the transaction, a new writer can be obtained from
// `ContentStore.Resume` using the same key. The write can then be continued
// from it was left off.
//
// To abandon a transaction completely, first call close then `Store.Remove` to
// clean up the associated resources.
func (cw *writer) Close() (err error) {
if err := unlock(cw.lock); err != nil {
log.L.Debug("unlock failed: %v", err)
}
if cw.fp != nil {
cw.fp.Sync()
return cw.fp.Close()
}
return nil
}
func (w *writer) Truncate(size int64) error {
if size != 0 {
return errors.New("Truncate: unsupported size")
}
w.offset = 0
w.digester.Hash().Reset()
return w.fp.Truncate(0)
}

120
vendor/github.com/containerd/containerd/fs/copy.go generated vendored Normal file
View File

@@ -0,0 +1,120 @@
package fs
import (
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/pkg/errors"
)
var (
bufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, 32*1024)
},
}
)
// 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(source, target); 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
}
func copyFile(source, target 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,80 @@
package fs
import (
"io"
"os"
"syscall"
"github.com/pkg/errors"
"github.com/stevvooe/continuity/sysx"
)
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 {
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)
}
}
if err := syscall.UtimesNano(name, []syscall.Timespec{st.Atim, st.Mtim}); 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")
}
n, err := sysx.CopyFileRange(src.Fd(), nil, dst.Fd(), nil, int(st.Size()), 0)
if err != nil {
if err != syscall.ENOSYS && err != syscall.EXDEV {
return errors.Wrap(err, "copy file range failed")
}
buf := bufferPool.Get().([]byte)
_, err = io.CopyBuffer(dst, src, buf)
bufferPool.Put(buf)
return err
}
if int64(n) != st.Size() {
return errors.Wrapf(err, "short copy: %d of %d", int64(n), st.Size())
}
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 syscall.Mknod(dst, uint32(fi.Mode()), int(st.Rdev))
}

View File

@@ -0,0 +1,33 @@
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")
}

301
vendor/github.com/containerd/containerd/fs/diff.go generated vendored Normal file
View File

@@ -0,0 +1,301 @@
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 (
// ChangeKindAdd represents an addition of
// a file
ChangeKindAdd = iota
// 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 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 rmdir == "" && 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 {
continue
}
}
if err := changeFn(k, p, f, nil); err != nil {
return err
}
}
return nil
})
return g.Wait()
}

View File

@@ -0,0 +1,92 @@
package fs
import (
"bytes"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/stevvooe/continuity/sysx"
)
// whiteouts are files with a special meaning for the layered filesystem.
// Docker uses AUFS whiteout files inside exported archives. In other
// filesystems these files are generated/handled on tar creation/extraction.
// whiteoutPrefix prefix means file is a whiteout. If this is followed by a
// filename this means that file has been removed from the base layer.
const whiteoutPrefix = ".wh."
// whiteoutMetaPrefix prefix means whiteout has a special meaning and is not
// for removing an actual file. Normally these files are excluded from exported
// archives.
const 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.
const whiteoutLinkDir = whiteoutMetaPrefix + "plnk"
// whiteoutOpaqueDir file means directory has been made opaque - meaning
// readdir calls to this directory do not follow to lower layers.
const whiteoutOpaqueDir = whiteoutMetaPrefix + ".opq"
// 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
}
func aufsMetadataSkip(path string) (skip bool, err error) {
skip, err = filepath.Match(string(os.PathSeparator)+whiteoutMetaPrefix+"*", path)
if err != nil {
skip = true
}
return
}
func aufsDeletedFile(root, path string, fi os.FileInfo) (string, error) {
f := filepath.Base(path)
// If there is a whiteout, then the file was removed
if strings.HasPrefix(f, whiteoutPrefix) {
originalFile := f[len(whiteoutPrefix):]
return filepath.Join(filepath.Dir(path), originalFile), nil
}
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 != syscall.ENODATA {
return false, errors.Wrapf(err, "failed to get xattr for %s", p1)
}
c2, err := sysx.LGetxattr(p2, "security.capability")
if err != nil && err != syscall.ENODATA {
return false, errors.Wrapf(err, "failed to get xattr for %s", p2)
}
return bytes.Equal(c1, c2), nil
}

View File

@@ -0,0 +1,15 @@
package fs
func detectDirDiff(upper, lower string) *diffDirOptions {
return nil
}
func compareSysStat(s1, s2 interface{}) (bool, error) {
// TODO: Use windows specific sys type
return false, nil
}
func compareCapabilities(p1, p2 string) (bool, error) {
// TODO: Use windows equivalent
return true, nil
}

12
vendor/github.com/containerd/containerd/fs/hardlink.go generated vendored Normal file
View File

@@ -0,0 +1,12 @@
package fs
import "os"
// 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) {
return getHardLink(name, fi, inodes)
}

View File

@@ -0,0 +1,33 @@
// +build !windows
package fs
import (
"errors"
"os"
"syscall"
)
func getHardLink(name string, fi os.FileInfo, inodes map[uint64]string) (string, error) {
if fi.IsDir() {
return "", nil
}
s, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return "", errors.New("unsupported stat type")
}
// If inode is not hardlinked, no reason to lookup or save inode
if s.Nlink == 1 {
return "", nil
}
inode := uint64(s.Ino)
path, ok := inodes[inode]
if !ok {
inodes[inode] = name
}
return path, nil
}

View File

@@ -0,0 +1,7 @@
package fs
import "os"
func getHardLink(string, os.FileInfo, map[uint64]string) (string, error) {
return "", nil
}

162
vendor/github.com/containerd/containerd/fs/path.go generated vendored Normal file
View File

@@ -0,0 +1,162 @@
package fs
import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"strings"
)
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 one of the
// files, check content of file to determine difference
if t1.Nanosecond() == 0 || t2.Nanosecond() == 0 {
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
}
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
}
}

13
vendor/github.com/containerd/containerd/fs/time.go generated vendored Normal file
View File

@@ -0,0 +1,13 @@
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))
}

View File

@@ -0,0 +1,144 @@
package images
import (
"context"
"encoding/json"
"fmt"
"github.com/containerd/containerd/content"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
var SkipDesc = fmt.Errorf("skip descriptor")
type Handler interface {
Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
}
type HandlerFunc func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
func (fn HandlerFunc) Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
return fn(ctx, desc)
}
// Handlers returns a handler that will run the handlers in sequence.
func Handlers(handlers ...Handler) HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
var children []ocispec.Descriptor
for _, handler := range handlers {
ch, err := handler.Handle(ctx, desc)
if err != nil {
return nil, err
}
children = append(children, ch...)
}
return children, nil
}
}
// Walk the resources of an image and call the handler for each. If the handler
// decodes the sub-resources for each image,
//
// This differs from dispatch in that each sibling resource is considered
// synchronously.
func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
for _, desc := range descs {
children, err := handler.Handle(ctx, desc)
if err != nil {
if errors.Cause(err) == SkipDesc {
return nil // don't traverse the children.
}
return err
}
if len(children) > 0 {
if err := Walk(ctx, handler, children...); err != nil {
return err
}
}
}
return nil
}
// Dispatch runs the provided handler for content specified by the descriptors.
// If the handler decode subresources, they will be visited, as well.
//
// Handlers for siblings are run in parallel on the provided descriptors. A
// handler may return `SkipDesc` to signal to the dispatcher to not traverse
// any children.
//
// Typically, this function will be used with `FetchHandler`, often composed
// with other handlers.
//
// If any handler returns an error, the dispatch session will be canceled.
func Dispatch(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
eg, ctx := errgroup.WithContext(ctx)
for _, desc := range descs {
desc := desc
eg.Go(func() error {
desc := desc
children, err := handler.Handle(ctx, desc)
if err != nil {
if errors.Cause(err) == SkipDesc {
return nil // don't traverse the children.
}
return err
}
if len(children) > 0 {
return Dispatch(ctx, handler, children...)
}
return nil
})
}
return eg.Wait()
}
// ChildrenHandler decodes well-known manifests types and returns their children.
//
// This is useful for supporting recursive fetch and other use cases where you
// want to do a full walk of resources.
//
// One can also replace this with another implementation to allow descending of
// arbitrary types.
func ChildrenHandler(provider content.Provider) HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch desc.MediaType {
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
case MediaTypeDockerSchema2Layer, MediaTypeDockerSchema2LayerGzip,
MediaTypeDockerSchema2Config:
return nil, nil
default:
return nil, fmt.Errorf("%v not yet supported", desc.MediaType)
}
p, err := content.ReadBlob(ctx, provider, desc.Digest)
if err != nil {
return nil, err
}
// TODO(stevvooe): We just assume oci manifest, for now. There may be
// subtle differences from the docker version.
var manifest ocispec.Manifest
if err := json.Unmarshal(p, &manifest); err != nil {
return nil, err
}
var descs []ocispec.Descriptor
descs = append(descs, manifest.Config)
descs = append(descs, manifest.Layers...)
return descs, nil
}
}

125
vendor/github.com/containerd/containerd/images/image.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
package images
import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Image provides the model for how containerd views container images.
type Image struct {
Name string
Target ocispec.Descriptor
}
// TODO(stevvooe): Many of these functions make strong platform assumptions,
// which are untrue in a lot of cases. More refactoring must be done here to
// make this work in all cases.
// Config resolves the image configuration descriptor.
//
// The caller can then use the descriptor to resolve and process the
// configuration of the image.
func (image *Image) Config(ctx context.Context, provider content.Provider) (ocispec.Descriptor, error) {
var configDesc ocispec.Descriptor
return configDesc, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch image.Target.MediaType {
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
rc, err := provider.Reader(ctx, image.Target.Digest)
if err != nil {
return nil, err
}
defer rc.Close()
p, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}
var manifest ocispec.Manifest
if err := json.Unmarshal(p, &manifest); err != nil {
return nil, err
}
configDesc = manifest.Config
return nil, nil
default:
return nil, errors.New("could not resolve config")
}
}), image.Target)
}
// RootFS returns the unpacked diffids that make up and images rootfs.
//
// These are used to verify that a set of layers unpacked to the expected
// values.
func (image *Image) RootFS(ctx context.Context, provider content.Provider) ([]digest.Digest, error) {
desc, err := image.Config(ctx, provider)
if err != nil {
return nil, err
}
p, err := content.ReadBlob(ctx, provider, desc.Digest)
if err != nil {
log.G(ctx).Fatal(err)
}
var config ocispec.Image
if err := json.Unmarshal(p, &config); err != nil {
log.G(ctx).Fatal(err)
}
// TODO(stevvooe): Remove this bit when OCI structure uses correct type for
// rootfs.DiffIDs.
var diffIDs []digest.Digest
for _, diffID := range config.RootFS.DiffIDs {
diffIDs = append(diffIDs, digest.Digest(diffID))
}
return diffIDs, nil
}
// Size returns the total size of an image's packed resources.
func (image *Image) Size(ctx context.Context, provider content.Provider) (int64, error) {
var size int64
return size, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch image.Target.MediaType {
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
size += desc.Size
rc, err := provider.Reader(ctx, image.Target.Digest)
if err != nil {
return nil, err
}
defer rc.Close()
p, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}
var manifest ocispec.Manifest
if err := json.Unmarshal(p, &manifest); err != nil {
return nil, err
}
size += manifest.Config.Size
for _, layer := range manifest.Layers {
size += layer.Size
}
return nil, nil
default:
return nil, errors.New("unsupported type")
}
}), image.Target)
}

View File

@@ -0,0 +1,13 @@
package images
// mediatype definitions for image components handled in containerd.
//
// oci components are generally referenced directly, although we may centralize
// here for clarity.
const (
MediaTypeDockerSchema2Layer = "application/vnd.docker.image.rootfs.diff.tar"
MediaTypeDockerSchema2LayerGzip = "application/vnd.docker.image.rootfs.diff.tar.gzip"
MediaTypeDockerSchema2Config = "application/vnd.docker.container.image.v1+json"
MediaTypeDockerSchema2Manifest = "application/vnd.docker.distribution.manifest.v2+json"
MediaTypeDockerSchema2ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
)

View File

@@ -0,0 +1,215 @@
package images
import (
"context"
"encoding/binary"
"fmt"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
var (
ErrExists = errors.New("images: exists")
ErrNotFound = errors.New("images: not found")
)
type Store interface {
Put(ctx context.Context, name string, desc ocispec.Descriptor) error
Get(ctx context.Context, name string) (Image, error)
List(ctx context.Context) ([]Image, error)
Delete(ctx context.Context, name string) error
}
// IsNotFound returns true if the error is due to a missing image.
func IsNotFound(err error) bool {
return errors.Cause(err) == ErrNotFound
}
func IsExists(err error) bool {
return errors.Cause(err) == ErrExists
}
var (
bucketKeyStorageVersion = []byte("v1")
bucketKeyImages = []byte("images")
bucketKeyDigest = []byte("digest")
bucketKeyMediaType = []byte("mediatype")
bucketKeySize = []byte("size")
)
// TODO(stevvooe): This file comprises the data required to implement the
// "metadata" store. For now, it is bound tightly to the local machine and bolt
// but we can take this and use it to define a service interface.
// InitDB will initialize the database for use. The database must be opened for
// write and the caller must not be holding an open transaction.
func InitDB(db *bolt.DB) error {
log.L.Debug("init db")
return db.Update(func(tx *bolt.Tx) error {
_, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyImages)
return err
})
}
func NewImageStore(tx *bolt.Tx) Store {
return &storage{tx: tx}
}
type storage struct {
tx *bolt.Tx
}
func (s *storage) Get(ctx context.Context, name string) (Image, error) {
var image Image
if err := withImageBucket(s.tx, name, func(bkt *bolt.Bucket) error {
image.Name = name
return readImage(&image, bkt)
}); err != nil {
return Image{}, err
}
return image, nil
}
func (s *storage) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
if err != nil {
return err
}
var (
buf [binary.MaxVarintLen64]byte
sizeEncoded []byte = buf[:]
)
sizeEncoded = sizeEncoded[:binary.PutVarint(sizeEncoded, desc.Size)]
if len(sizeEncoded) == 0 {
return fmt.Errorf("failed encoding size = %v", desc.Size)
}
for _, v := range [][2][]byte{
{bucketKeyDigest, []byte(desc.Digest)},
{bucketKeyMediaType, []byte(desc.MediaType)},
{bucketKeySize, sizeEncoded},
} {
if err := ibkt.Put(v[0], v[1]); err != nil {
return err
}
}
return nil
})
}
func (s *storage) List(ctx context.Context) ([]Image, error) {
var images []Image
if err := withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
var (
image = Image{
Name: string(k),
}
kbkt = bkt.Bucket(k)
)
if err := readImage(&image, kbkt); err != nil {
return err
}
images = append(images, image)
return nil
})
}); err != nil {
return nil, err
}
return images, nil
}
func (s *storage) Delete(ctx context.Context, name string) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
return bkt.DeleteBucket([]byte(name))
})
}
func readImage(image *Image, bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
if v == nil {
return nil // skip it? a bkt maybe?
}
// TODO(stevvooe): This is why we need to use byte values for
// keys, rather than full arrays.
switch string(k) {
case string(bucketKeyDigest):
image.Target.Digest = digest.Digest(v)
case string(bucketKeyMediaType):
image.Target.MediaType = string(v)
case string(bucketKeySize):
image.Target.Size, _ = binary.Varint(v)
}
return nil
})
}
func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error) {
bkt, err := tx.CreateBucketIfNotExists(keys[0])
if err != nil {
return nil, err
}
for _, key := range keys[1:] {
bkt, err = bkt.CreateBucketIfNotExists(key)
if err != nil {
return nil, err
}
}
return bkt, nil
}
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
bkt := getImagesBucket(tx)
if bkt == nil {
return ErrNotFound
}
return fn(bkt)
}
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
bkt := getImageBucket(tx, name)
if bkt == nil {
return ErrNotFound
}
return fn(bkt)
}
func getImagesBucket(tx *bolt.Tx) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages)
}
func getImageBucket(tx *bolt.Tx, name string) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages, []byte(name))
}
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
bkt := tx.Bucket(keys[0])
for _, key := range keys[1:] {
if bkt == nil {
break
}
bkt = bkt.Bucket(key)
}
return bkt
}

81
vendor/github.com/containerd/containerd/log/context.go generated vendored Normal file
View File

@@ -0,0 +1,81 @@
package log
import (
"context"
"path"
"github.com/Sirupsen/logrus"
)
var (
// G is an alias for GetLogger.
//
// We may want to define this locally to a package to get package tagged log
// messages.
G = GetLogger
// L is an alias for the the standard logger.
L = logrus.NewEntry(logrus.StandardLogger())
)
type (
loggerKey struct{}
moduleKey struct{}
)
// WithLogger returns a new context with the provided logger. Use in
// combination with logger.WithField(s) for great effect.
func WithLogger(ctx context.Context, logger *logrus.Entry) context.Context {
return context.WithValue(ctx, loggerKey{}, logger)
}
// GetLogger retrieves the current logger from the context. If no logger is
// available, the default logger is returned.
func GetLogger(ctx context.Context) *logrus.Entry {
logger := ctx.Value(loggerKey{})
if logger == nil {
return L
}
return logger.(*logrus.Entry)
}
// WithModule adds the module to the context, appending it with a slash if a
// module already exists. A module is just an roughly correlated defined by the
// call tree for a given context.
//
// As an example, we might have a "node" module already part of a context. If
// this function is called with "tls", the new value of module will be
// "node/tls".
//
// Modules represent the call path. If the new module and last module are the
// same, a new module entry will not be created. If the new module and old
// older module are the same but separated by other modules, the cycle will be
// represented by the module path.
func WithModule(ctx context.Context, module string) context.Context {
parent := GetModulePath(ctx)
if parent != "" {
// don't re-append module when module is the same.
if path.Base(parent) == module {
return ctx
}
module = path.Join(parent, module)
}
ctx = WithLogger(ctx, GetLogger(ctx).WithField("module", module))
return context.WithValue(ctx, moduleKey{}, module)
}
// GetModulePath returns the module path for the provided context. If no module
// is set, an empty string is returned.
func GetModulePath(ctx context.Context) string {
module := ctx.Value(moduleKey{})
if module == nil {
return ""
}
return module.(string)
}

14
vendor/github.com/containerd/containerd/log/grpc.go generated vendored Normal file
View File

@@ -0,0 +1,14 @@
package log
import (
"context"
"google.golang.org/grpc/grpclog"
)
func init() {
ctx := WithModule(context.Background(), "grpc")
// completely replace the grpc logger with the logrus logger.
grpclog.SetLogger(G(ctx))
}

View File

@@ -0,0 +1,54 @@
package plugin
import "github.com/containerd/containerd"
// ContainerMonitor provides an interface for monitoring of containers within containerd
type ContainerMonitor interface {
// Monitor adds the provided container to the monitor
Monitor(containerd.Container) error
// Stop stops and removes the provided container from the monitor
Stop(containerd.Container) error
}
func NewMultiContainerMonitor(monitors ...ContainerMonitor) ContainerMonitor {
return &multiContainerMonitor{
monitors: monitors,
}
}
func NewNoopMonitor() ContainerMonitor {
return &noopContainerMonitor{}
}
type noopContainerMonitor struct {
}
func (mm *noopContainerMonitor) Monitor(c containerd.Container) error {
return nil
}
func (mm *noopContainerMonitor) Stop(c containerd.Container) error {
return nil
}
type multiContainerMonitor struct {
monitors []ContainerMonitor
}
func (mm *multiContainerMonitor) Monitor(c containerd.Container) error {
for _, m := range mm.monitors {
if err := m.Monitor(c); err != nil {
return err
}
}
return nil
}
func (mm *multiContainerMonitor) Stop(c containerd.Container) error {
for _, m := range mm.monitors {
if err := m.Stop(c); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,81 @@
package plugin
import (
"fmt"
"sync"
"github.com/boltdb/bolt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/snapshot"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
type PluginType int
const (
RuntimePlugin PluginType = iota + 1
GRPCPlugin
SnapshotPlugin
ContainerMonitorPlugin
)
type Registration struct {
Type PluginType
Config interface{}
Init func(*InitContext) (interface{}, error)
}
// TODO(@crosbymichael): how to we keep this struct from growing but support dependency injection for loaded plugins?
type InitContext struct {
Root string
State string
Runtimes map[string]containerd.Runtime
Content *content.Store
Meta *bolt.DB
Snapshotter snapshot.Snapshotter
Config interface{}
Context context.Context
Monitor ContainerMonitor
}
type Service interface {
Register(*grpc.Server) error
}
var register = struct {
sync.Mutex
r map[string]*Registration
}{
r: make(map[string]*Registration),
}
// Load loads all plugins at the provided path into containerd
func Load(path string) (err error) {
defer func() {
if v := recover(); v != nil {
rerr, ok := v.(error)
if !ok {
rerr = fmt.Errorf("%s", v)
}
err = rerr
}
}()
return loadPlugins(path)
}
func Register(name string, r *Registration) error {
register.Lock()
defer register.Unlock()
if _, ok := register.r[name]; ok {
return fmt.Errorf("plugin already registered as %q", name)
}
register.r[name] = r
return nil
}
func Registrations() map[string]*Registration {
return register.r
}

View File

@@ -0,0 +1,46 @@
// +build go1.8,!windows,amd64
package plugin
import (
"fmt"
"path/filepath"
"plugin"
"runtime"
)
// loadPlugins loads all plugins for the OS and Arch
// that containerd is built for inside the provided path
func loadPlugins(path string) error {
abs, err := filepath.Abs(path)
if err != nil {
return err
}
pattern := filepath.Join(abs, fmt.Sprintf(
"*-%s-%s.%s",
runtime.GOOS,
runtime.GOARCH,
getLibExt(),
))
libs, err := filepath.Glob(pattern)
if err != nil {
return err
}
for _, lib := range libs {
if _, err := plugin.Open(lib); err != nil {
return err
}
}
return nil
}
// getLibExt returns a platform specific lib extension for
// the platform that containerd is running on
func getLibExt() string {
switch runtime.GOOS {
case "windows":
return "dll"
default:
return "so"
}
}

View File

@@ -0,0 +1,8 @@
// +build !go1.8 windows !amd64
package plugin
func loadPlugins(path string) error {
// plugins not supported until 1.8
return nil
}

View File

@@ -0,0 +1,65 @@
package progress
import (
"bytes"
"fmt"
)
// TODO(stevvooe): We may want to support more interesting parameterization of
// the bar. For now, it is very simple.
// Bar provides a very simple progress bar implementation.
//
// Use with fmt.Printf and "r" to format the progress bar. A "-" flag makes it
// progress from right to left.
type Bar float64
var _ fmt.Formatter = Bar(1.0)
func (h Bar) Format(state fmt.State, r rune) {
switch r {
case 'r':
default:
panic(fmt.Sprintf("%v: unexpected format character", float64(h)))
}
if h > 1.0 {
h = 1.0
}
if h < 0.0 {
h = 0.0
}
if state.Flag('-') {
h = 1.0 - h
}
width, ok := state.Width()
if !ok {
// default width of 40
width = 40
}
var pad int
extra := len([]byte(green)) + len([]byte(reset))
p := make([]byte, width+extra)
p[0], p[len(p)-1] = '|', '|'
pad += 2
positive := int(Bar(width-pad) * h)
negative := width - pad - positive
n := 1
n += copy(p[n:], []byte(green))
n += copy(p[n:], bytes.Repeat([]byte("+"), positive))
n += copy(p[n:], []byte(reset))
if negative > 0 {
n += copy(p[n:len(p)-1], bytes.Repeat([]byte("-"), negative))
}
state.Write(p)
}

View File

@@ -0,0 +1,2 @@
// Package progress assists in displaying human readable progress information.
package progress

View File

@@ -0,0 +1,8 @@
package progress
const (
escape = "\x1b"
reset = escape + "[0m"
red = escape + "[31m"
green = escape + "[32m"
)

View File

@@ -0,0 +1,25 @@
package progress
import (
"fmt"
"time"
units "github.com/docker/go-units"
)
// Bytes converts a regular int64 to human readable type.
type Bytes int64
func (b Bytes) String() string {
return units.CustomSize("%02.1f %s", float64(b), 1024.0, []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"})
}
type BytesPerSecond int64
func NewBytesPerSecond(n int64, duration time.Duration) BytesPerSecond {
return BytesPerSecond(float64(n) / duration.Seconds())
}
func (bps BytesPerSecond) String() string {
return fmt.Sprintf("%v/s", Bytes(bps))
}

View File

@@ -0,0 +1,60 @@
package progress
import (
"bytes"
"fmt"
"io"
)
// Writer buffers writes until flush, at which time the last screen is cleared
// and the current buffer contents are written. This is useful for
// implementing progress displays, such as those implemented in docker and
// git.
type Writer struct {
buf bytes.Buffer
w io.Writer
lines int
}
func NewWriter(w io.Writer) *Writer {
return &Writer{
w: w,
}
}
func (w *Writer) Write(p []byte) (n int, err error) {
return w.buf.Write(p)
}
// Flush should be called when refreshing the current display.
func (w *Writer) Flush() error {
if w.buf.Len() == 0 {
return nil
}
if err := w.clear(); err != nil {
return err
}
w.lines = bytes.Count(w.buf.Bytes(), []byte("\n"))
if _, err := w.w.Write(w.buf.Bytes()); err != nil {
return err
}
w.buf.Reset()
return nil
}
// TODO(stevvooe): The following are system specific. Break these out if we
// decide to build this package further.
func (w *Writer) clear() error {
for i := 0; i < w.lines; i++ {
if _, err := fmt.Fprintf(w.w, "\x1b[0A\x1b[2K\r"); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,141 @@
package reference
import (
"errors"
"fmt"
"net/url"
"path"
"regexp"
"strings"
digest "github.com/opencontainers/go-digest"
)
var (
ErrInvalid = errors.New("invalid reference")
ErrObjectRequired = errors.New("object required")
ErrHostnameRequired = errors.New("hostname required")
)
// Spec defines the main components of a reference specification.
//
// A reference specification is a schema-less URI parsed into common
// components. The two main components, locator and object, are required to be
// supported by remotes. It represents a superset of the naming define in
// docker's reference schema. It aims to be compatible but not prescriptive.
//
// While the interpretation of the components, locator and object, are up to
// the remote, we define a few common parts, accessible via helper methods.
//
// The first is the hostname, which is part of the locator. This doesn't need
// to map to a physical resource, but it must parse as a hostname. We refer to
// this as the namespace.
//
// The other component made accessible by helper method is the digest. This is
// part of the object identifier, always prefixed with an '@'. If present, the
// remote may use the digest portion directly or resolve it against a prefix.
// If the object does not include the `@` symbol, the return value for `Digest`
// will be empty.
type Spec struct {
// Locator is the host and path portion of the specification. The host
// portion may refer to an actual host or just a namespace of related
// images.
//
// Typically, the locator may used to resolve the remote to fetch specific
// resources.
Locator string
// Object contains the identifier for the remote resource. Classically,
// this is a tag but can refer to anything in a remote. By convention, any
// portion that may be a partial or whole digest will be preceeded by an
// `@`. Anything preceeding the `@` will be referred to as the "tag".
//
// In practice, we will see this broken down into the following formats:
//
// 1. <tag>
// 2. <tag>@<digest spec>
// 3. @<digest spec>
//
// We define the tag to be anything except '@' and ':'. <digest spec> may
// be a full valid digest or shortened version, possibly with elided
// algorithm.
Object string
}
var splitRe = regexp.MustCompile(`[:@]`)
// Parse parses the string into a structured ref.
func Parse(s string) (Spec, error) {
u, err := url.Parse("dummy://" + s)
if err != nil {
return Spec{}, err
}
if u.Scheme != "dummy" {
return Spec{}, ErrInvalid
}
if u.Host == "" {
return Spec{}, ErrHostnameRequired
}
parts := splitRe.Split(u.Path, 2)
if len(parts) < 2 {
return Spec{}, ErrObjectRequired
}
// This allows us to retain the @ to signify digests or shortend digests in
// the object.
object := u.Path[len(parts[0]):]
if object[:1] == ":" {
object = object[1:]
}
return Spec{
Locator: path.Join(u.Host, parts[0]),
Object: object,
}, nil
}
// Hostname returns the hostname portion of the locator.
//
// Remotes are not required to directly access the resources at this host. This
// method is provided for convenience.
func (r Spec) Hostname() string {
i := strings.Index(r.Locator, "/")
if i < 0 {
i = len(r.Locator) + 1
}
return r.Locator[:i]
}
// Digest returns the digest portion of the reference spec. This may be a
// partial or invalid digest, which may be used to lookup a complete digest.
func (r Spec) Digest() digest.Digest {
_, dgst := SplitObject(r.Object)
return dgst
}
// String returns the normalized string for the ref.
func (r Spec) String() string {
if r.Object[:1] == "@" {
return fmt.Sprintf("%v%v", r.Locator, r.Object)
}
return fmt.Sprintf("%v:%v", r.Locator, r.Object)
}
// SplitObject provides two parts of the object spec, delimiited by an `@`
// symbol.
//
// Either may be empty and it is the callers job to validate them
// appropriately.
func SplitObject(obj string) (tag string, dgst digest.Digest) {
parts := strings.SplitAfterN(obj, "@", 2)
if len(parts) < 2 {
return parts[0], ""
} else {
return parts[0], digest.Digest(parts[1])
}
}

View File

@@ -0,0 +1,293 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"github.com/Sirupsen/logrus"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/reference"
"github.com/containerd/containerd/remotes"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/net/context/ctxhttp"
)
// NOTE(stevvooe): Most of the code below this point is prototype code to
// demonstrate a very simplified docker.io fetcher. We have a lot of hard coded
// values but we leave many of the details down to the fetcher, creating a lot
// of room for ways to fetch content.
type dockerResolver struct{}
func NewResolver() remotes.Resolver {
return &dockerResolver{}
}
var _ remotes.Resolver = &dockerResolver{}
func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, remotes.Fetcher, error) {
refspec, err := reference.Parse(ref)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
var (
base url.URL
token string
)
switch refspec.Hostname() {
case "docker.io":
base.Scheme = "https"
base.Host = "registry-1.docker.io"
prefix := strings.TrimPrefix(refspec.Locator, "docker.io/")
base.Path = path.Join("/v2", prefix)
token, err = getToken(ctx, "repository:"+prefix+":pull")
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
case "localhost:5000":
base.Scheme = "http"
base.Host = "localhost:5000"
base.Path = path.Join("/v2", strings.TrimPrefix(refspec.Locator, "localhost:5000/"))
default:
return "", ocispec.Descriptor{}, nil, errors.Errorf("unsupported locator: %q", refspec.Locator)
}
fetcher := &dockerFetcher{
base: base,
token: token,
}
var (
urls []string
dgst = refspec.Digest()
)
if dgst != "" {
if err := dgst.Validate(); err != nil {
// need to fail here, since we can't actually resolve the invalid
// digest.
return "", ocispec.Descriptor{}, nil, err
}
// turns out, we have a valid digest, make a url.
urls = append(urls, fetcher.url("manifests", dgst.String()))
} else {
urls = append(urls, fetcher.url("manifests", refspec.Object))
}
// fallback to blobs on not found.
urls = append(urls, fetcher.url("blobs", dgst.String()))
for _, u := range urls {
req, err := http.NewRequest(http.MethodHead, u, nil)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
// set headers for all the types we support for resolution.
req.Header.Set("Accept", strings.Join([]string{
images.MediaTypeDockerSchema2Manifest,
images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest,
ocispec.MediaTypeImageIndex, "*"}, ", "))
log.G(ctx).Debug("resolving")
resp, err := fetcher.doRequest(ctx, req)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
resp.Body.Close() // don't care about body contents.
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
continue
}
return "", ocispec.Descriptor{}, nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
}
// this is the only point at which we trust the registry. we use the
// content headers to assemble a descriptor for the name. when this becomes
// more robust, we mostly get this information from a secure trust store.
dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
if dgstHeader != "" {
if err := dgstHeader.Validate(); err != nil {
if err == nil {
return "", ocispec.Descriptor{}, nil, errors.Errorf("%q in header not a valid digest", dgstHeader)
}
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
}
dgst = dgstHeader
}
if dgst == "" {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "could not resolve digest for %v", ref)
}
var (
size int64
sizeHeader = resp.Header.Get("Content-Length")
)
size, err = strconv.ParseInt(sizeHeader, 10, 64)
if err != nil || size < 0 {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid size", sizeHeader)
}
desc := ocispec.Descriptor{
Digest: dgst,
MediaType: resp.Header.Get("Content-Type"), // need to strip disposition?
Size: size,
}
log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
return ref, desc, fetcher, nil
}
return "", ocispec.Descriptor{}, nil, errors.Errorf("%v not found", ref)
}
type dockerFetcher struct {
base url.URL
token string
}
func (r *dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(
logrus.Fields{
"base": r.base.String(),
"digest": desc.Digest,
},
))
paths, err := getV2URLPaths(desc)
if err != nil {
return nil, err
}
for _, path := range paths {
u := r.url(path)
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
resp, err := r.doRequest(ctx, req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
continue // try one of the other urls.
}
resp.Body.Close()
return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
}
return resp.Body, nil
}
return nil, errors.New("not found")
}
func (r *dockerFetcher) url(ps ...string) string {
url := r.base
url.Path = path.Join(url.Path, path.Join(ps...))
return url.String()
}
func (r *dockerFetcher) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
log.G(ctx).WithField("request.headers", req.Header).Debug("fetch content")
if r.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
}
resp, err := ctxhttp.Do(ctx, http.DefaultClient, req)
if err != nil {
return nil, err
}
log.G(ctx).WithFields(logrus.Fields{
"status": resp.Status,
"response.headers": resp.Header,
}).Debug("fetch response received")
return resp, err
}
func getToken(ctx context.Context, scopes ...string) (string, error) {
var (
u = url.URL{
Scheme: "https",
Host: "auth.docker.io",
Path: "/token",
}
q = url.Values{
"scope": scopes,
"service": []string{"registry.docker.io"}, // usually comes from auth challenge
}
)
u.RawQuery = q.Encode()
log.G(ctx).WithField("token.url", u.String()).Debug("requesting token")
resp, err := ctxhttp.Get(ctx, http.DefaultClient, u.String())
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return "", errors.Errorf("unexpected status code: %v %v", resp.StatusCode, resp.Status)
}
p, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tokenResponse struct {
Token string `json:"token"`
}
if err := json.Unmarshal(p, &tokenResponse); err != nil {
return "", err
}
return tokenResponse.Token, nil
}
// getV2URLPaths generates the candidate urls paths for the object based on the
// set of hints and the provided object id. URLs are returned in the order of
// most to least likely succeed.
func getV2URLPaths(desc ocispec.Descriptor) ([]string, error) {
var urls []string
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
urls = append(urls, path.Join("manifests", desc.Digest.String()))
}
// always fallback to attempting to get the object out of the blobs store.
urls = append(urls, path.Join("blobs", desc.Digest.String()))
return urls, nil
}

View File

@@ -0,0 +1,66 @@
package remotes
import (
"context"
"fmt"
"github.com/Sirupsen/logrus"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// MakeRef returns a unique reference for the descriptor. This reference can be
// used to lookup ongoing processes related to the descriptor. This function
// may look to the context to namespace the reference appropriately.
func MakeRefKey(ctx context.Context, desc ocispec.Descriptor) string {
// TODO(stevvooe): Need better remote key selection here. Should be a
// product of the context, which may include information about the ongoing
// fetch process.
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest,
images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
return "manifest-" + desc.Digest.String()
case images.MediaTypeDockerSchema2Layer, images.MediaTypeDockerSchema2LayerGzip:
return "layer-" + desc.Digest.String()
case "application/vnd.docker.container.image.v1+json":
return "config-" + desc.Digest.String()
default:
log.G(ctx).Warnf("reference for unknown type: %s", desc.MediaType)
return "unknown-" + desc.Digest.String()
}
}
// FetchHandler returns a handler that will fetch all content into the ingester
// discovered in a call to Dispatch. Use with ChildrenHandler to do a full
// recursive fetch.
func FetchHandler(ingester content.Ingester, fetcher Fetcher) images.HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(logrus.Fields{
"digest": desc.Digest,
"mediatype": desc.MediaType,
"size": desc.Size,
}))
switch desc.MediaType {
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
return nil, fmt.Errorf("%v not yet supported", desc.MediaType)
default:
err := fetch(ctx, ingester, fetcher, desc)
return nil, err
}
}
}
func fetch(ctx context.Context, ingester content.Ingester, fetcher Fetcher, desc ocispec.Descriptor) error {
log.G(ctx).Debug("fetch")
ref := MakeRefKey(ctx, desc)
rc, err := fetcher.Fetch(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
return content.WriteBlob(ctx, ingester, ref, rc, desc.Size, desc.Digest)
}

View File

@@ -0,0 +1,31 @@
package remotes
import "strings"
// HintExists returns true if a hint of the provided kind and values exists in
// the set of provided hints.
func HintExists(kind, value string, hints ...string) bool {
for _, hint := range hints {
if strings.HasPrefix(hint, kind) && strings.HasSuffix(hint, value) {
return true
}
}
return false
}
// HintValues returns a slice of the values of the hints that match kind.
func HintValues(kind string, hints ...string) []string {
var values []string
for _, hint := range hints {
if strings.HasPrefix(hint, kind) {
parts := strings.SplitN(hint, ":", 2)
if len(parts) < 2 {
continue
}
values = append(values, parts[1])
}
}
return values
}

View File

@@ -0,0 +1,37 @@
package remotes
import (
"context"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Resolver provides a remote based on a locator.
type Resolver interface {
// Resolve attempts to resolve the reference into a name and descriptor.
//
// The argument `ref` should be a scheme-less URI representing the remote.
// Structurally, it has a host and path. The "host" can be used to directly
// reference a specific host or be matched against a specific handler.
//
// The returned name should be used to identify the referenced entity.
// Dependending on the remote namespace, this may be immutable or mutable.
// While the name may differ from ref, it should itself be a valid ref.
//
// If the resolution fails, an error will be returned.
Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, fetcher Fetcher, err error)
}
type Fetcher interface {
// Fetch the resource identified by the descriptor.
Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
}
// FetcherFunc allows package users to implement a Fetcher with just a
// function.
type FetcherFunc func(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
func (fn FetcherFunc) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
return fn(ctx, desc)
}

151
vendor/github.com/containerd/containerd/rootfs/apply.go generated vendored Normal file
View File

@@ -0,0 +1,151 @@
package rootfs
import (
"context"
"io"
"io/ioutil"
"github.com/containerd/containerd"
"github.com/containerd/containerd/archive"
"github.com/containerd/containerd/archive/compression"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/snapshot"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type Unpacker interface {
Unpack(ctx context.Context, layers []ocispec.Descriptor) (digest.Digest, error)
}
type Mounter interface {
Mount(target string, mounts ...containerd.Mount) error
Unmount(target string) error
}
// ApplyLayer applies the layer to the provided parent. The resulting snapshot
// will be stored under its ChainID.
//
// The parent *must* be the chainID of the parent layer.
//
// The returned digest is the diffID for the applied layer.
func ApplyLayer(snapshots snapshot.Snapshotter, mounter Mounter, rd io.Reader, parent digest.Digest) (digest.Digest, error) {
ctx := context.TODO()
// create a temporary directory to work from, needs to be on same
// filesystem. Probably better if this shared but we'll use a tempdir, for
// now.
dir, err := ioutil.TempDir("", "unpack-")
if err != nil {
return "", errors.Wrapf(err, "creating temporary directory failed")
}
// TODO(stevvooe): Choose this key WAY more carefully. We should be able to
// create collisions for concurrent, conflicting unpack processes but we
// would need to have it be a function of the parent diffID and child
// layerID (since we don't know the diffID until we are done!).
key := dir
mounts, err := snapshots.Prepare(ctx, key, parent.String())
if err != nil {
return "", err
}
if err := mounter.Mount(dir, mounts...); err != nil {
if err := snapshots.Remove(ctx, key); err != nil {
log.L.WithError(err).Error("snapshot rollback failed")
}
return "", err
}
defer mounter.Unmount(dir)
rd, err = compression.DecompressStream(rd)
if err != nil {
return "", err
}
digester := digest.Canonical.Digester() // used to calculate diffID.
rd = io.TeeReader(rd, digester.Hash())
if _, err := archive.Apply(context.Background(), key, rd); err != nil {
return "", err
}
diffID := digester.Digest()
chainID := diffID
if parent != "" {
chainID = identity.ChainID([]digest.Digest{parent, chainID})
}
if _, err := snapshots.Stat(ctx, chainID.String()); err == nil {
return diffID, nil //TODO: call snapshots.Remove(ctx, key) once implemented
}
return diffID, snapshots.Commit(ctx, chainID.String(), key)
}
// Prepare the root filesystem from the set of layers. Snapshots are created
// for each layer if they don't exist, keyed by their chain id. If the snapshot
// already exists, it will be skipped.
//
// If successful, the chainID for the top-level layer is returned. That
// identifier can be used to check out a snapshot.
func Prepare(ctx context.Context, snapshots snapshot.Snapshotter, mounter Mounter, layers []ocispec.Descriptor,
// TODO(stevvooe): The following functions are candidate for internal
// object functions. We can use these to formulate the beginnings of a
// rootfs Controller.
//
// Just pass them in for now.
openBlob func(context.Context, digest.Digest) (io.ReadCloser, error),
resolveDiffID func(digest.Digest) digest.Digest,
registerDiffID func(diffID, dgst digest.Digest) error) (digest.Digest, error) {
var (
parent digest.Digest
chain []digest.Digest
)
for _, layer := range layers {
// TODO: layer.Digest should not be string
// (https://github.com/opencontainers/image-spec/pull/514)
layerDigest := digest.Digest(layer.Digest)
// This will convert a possibly compressed layer hash to the
// uncompressed hash, if we know about it. If we don't, we unpack and
// calculate it. If we do have it, we then calculate the chain id for
// the application and see if the snapshot is there.
diffID := resolveDiffID(layerDigest)
if diffID != "" {
chainLocal := append(chain, diffID)
chainID := identity.ChainID(chainLocal)
if _, err := snapshots.Stat(ctx, chainID.String()); err == nil {
continue
}
}
rc, err := openBlob(ctx, layerDigest)
if err != nil {
return "", err
}
defer rc.Close() // pretty lazy!
diffID, err = ApplyLayer(snapshots, mounter, rc, parent)
if err != nil {
return "", err
}
// Register the association between the diffID and the layer's digest.
// For uncompressed layers, this will be the same. For compressed
// layers, we can look up the diffID from the digest if we've already
// unpacked it.
if err := registerDiffID(diffID, layerDigest); err != nil {
return "", err
}
chain = append(chain, diffID)
parent = identity.ChainID(chain)
}
return parent, nil
}

94
vendor/github.com/containerd/containerd/rootfs/init.go generated vendored Normal file
View File

@@ -0,0 +1,94 @@
package rootfs
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/containerd/containerd"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/snapshot"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
var (
initializers = map[string]initializerFunc{}
)
type initializerFunc func(string) error
func InitRootFS(ctx context.Context, name string, parent digest.Digest, readonly bool, snapshotter snapshot.Snapshotter, mounter Mounter) ([]containerd.Mount, error) {
_, err := snapshotter.Stat(ctx, name)
if err == nil {
return nil, errors.Errorf("rootfs already exists")
}
// TODO: ensure not exist error once added to snapshot package
parentS := parent.String()
initName := defaultInitializer
initFn := initializers[initName]
if initFn != nil {
parentS, err = createInitLayer(ctx, parentS, initName, initFn, snapshotter, mounter)
if err != nil {
return nil, err
}
}
if readonly {
return snapshotter.View(ctx, name, parentS)
}
return snapshotter.Prepare(ctx, name, parentS)
}
func createInitLayer(ctx context.Context, parent, initName string, initFn func(string) error, snapshotter snapshot.Snapshotter, mounter Mounter) (string, error) {
initS := fmt.Sprintf("%s %s", parent, initName)
if _, err := snapshotter.Stat(ctx, initS); err == nil {
return initS, nil
}
// TODO: ensure not exist error once added to snapshot package
// Create tempdir
td, err := ioutil.TempDir("", "create-init-")
if err != nil {
return "", err
}
defer os.RemoveAll(td)
mounts, err := snapshotter.Prepare(ctx, td, parent)
if err != nil {
return "", err
}
defer func() {
if err != nil {
// TODO: once implemented uncomment
//if rerr := snapshotter.Remove(ctx, td); rerr != nil {
// log.G(ctx).Errorf("Failed to remove snapshot %s: %v", td, merr)
//}
}
}()
if err = mounter.Mount(td, mounts...); err != nil {
return "", err
}
if err = initFn(td); err != nil {
if merr := mounter.Unmount(td); merr != nil {
log.G(ctx).Errorf("Failed to unmount %s: %v", td, merr)
}
return "", err
}
if err = mounter.Unmount(td); err != nil {
return "", err
}
if err := snapshotter.Commit(ctx, initS, td); err != nil {
return "", err
}
return initS, nil
}

View File

@@ -0,0 +1,114 @@
package rootfs
import (
"os"
"path/filepath"
"syscall"
)
const (
defaultInitializer = "linux-init"
)
func init() {
initializers[defaultInitializer] = initFS
}
func createDirectory(name string, uid, gid int) initializerFunc {
return func(root string) error {
dname := filepath.Join(root, name)
st, err := os.Stat(dname)
if err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
if st.IsDir() {
stat := st.Sys().(*syscall.Stat_t)
if int(stat.Gid) == gid && int(stat.Uid) == uid {
return nil
}
} else {
if err := os.Remove(dname); err != nil {
return err
}
if err := os.Mkdir(dname, 0755); err != nil {
return err
}
}
} else {
if err := os.Mkdir(dname, 0755); err != nil {
return err
}
}
return os.Chown(dname, uid, gid)
}
}
func touchFile(name string, uid, gid int) initializerFunc {
return func(root string) error {
fname := filepath.Join(root, name)
st, err := os.Stat(fname)
if err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
stat := st.Sys().(*syscall.Stat_t)
if int(stat.Gid) == gid && int(stat.Uid) == uid {
return nil
}
return os.Chown(fname, uid, gid)
}
f, err := os.OpenFile(fname, os.O_CREATE, 0644)
if err != nil {
return err
}
defer f.Close()
return f.Chown(uid, gid)
}
}
func symlink(oldname, newname string) initializerFunc {
return func(root string) error {
linkName := filepath.Join(root, newname)
if _, err := os.Stat(linkName); err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
return nil
}
return os.Symlink(oldname, linkName)
}
}
func initFS(root string) error {
st, err := os.Stat(root)
if err != nil {
return err
}
stat := st.Sys().(*syscall.Stat_t)
uid := int(stat.Uid)
gid := int(stat.Gid)
initFuncs := []initializerFunc{
createDirectory("/dev", uid, gid),
createDirectory("/dev/pts", uid, gid),
createDirectory("/dev/shm", uid, gid),
touchFile("/dev/console", uid, gid),
createDirectory("/proc", uid, gid),
createDirectory("/sys", uid, gid),
createDirectory("/etc", uid, gid),
touchFile("/etc/resolv.conf", uid, gid),
touchFile("/etc/hosts", uid, gid),
touchFile("/etc/hostname", uid, gid),
symlink("/proc/mounts", "/etc/mtab"),
}
for _, fn := range initFuncs {
if err := fn(root); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,7 @@
// +build !linux
package rootfs
const (
defaultInitializer = ""
)

View File

@@ -0,0 +1,19 @@
package content
import (
"github.com/containerd/containerd/content"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
func rewriteGRPCError(err error) error {
switch grpc.Code(errors.Cause(err)) {
case codes.AlreadyExists:
return content.ErrExists
case codes.NotFound:
return content.ErrNotFound
}
return err
}

View File

@@ -0,0 +1,163 @@
package content
import (
"context"
"io"
contentapi "github.com/containerd/containerd/api/services/content"
"github.com/containerd/containerd/content"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
func NewIngesterFromClient(client contentapi.ContentClient) content.Ingester {
return &remoteIngester{
client: client,
}
}
type remoteIngester struct {
client contentapi.ContentClient
}
func (ri *remoteIngester) Writer(ctx context.Context, ref string, size int64, expected digest.Digest) (content.Writer, error) {
wrclient, offset, err := ri.negotiate(ctx, ref, size, expected)
if err != nil {
return nil, rewriteGRPCError(err)
}
return &remoteWriter{
client: wrclient,
offset: offset,
}, nil
}
func (ri *remoteIngester) negotiate(ctx context.Context, ref string, size int64, expected digest.Digest) (contentapi.Content_WriteClient, int64, error) {
wrclient, err := ri.client.Write(ctx)
if err != nil {
return nil, 0, err
}
if err := wrclient.Send(&contentapi.WriteRequest{
Action: contentapi.WriteActionStat,
Ref: ref,
Total: size,
Expected: expected,
}); err != nil {
return nil, 0, err
}
resp, err := wrclient.Recv()
if err != nil {
return nil, 0, err
}
return wrclient, resp.Offset, nil
}
type remoteWriter struct {
ref string
client contentapi.Content_WriteClient
offset int64
digest digest.Digest
}
func newRemoteWriter(client contentapi.Content_WriteClient, ref string, offset int64) (*remoteWriter, error) {
return &remoteWriter{
ref: ref,
client: client,
offset: offset,
}, nil
}
// send performs a synchronous req-resp cycle on the client.
func (rw *remoteWriter) send(req *contentapi.WriteRequest) (*contentapi.WriteResponse, error) {
if err := rw.client.Send(req); err != nil {
return nil, err
}
resp, err := rw.client.Recv()
if err == nil {
// try to keep these in sync
if resp.Digest != "" {
rw.digest = resp.Digest
}
}
return resp, err
}
func (rw *remoteWriter) Status() (content.Status, error) {
resp, err := rw.send(&contentapi.WriteRequest{
Action: contentapi.WriteActionStat,
})
if err != nil {
return content.Status{}, err
}
return content.Status{
Ref: rw.ref,
Offset: resp.Offset,
StartedAt: resp.StartedAt,
UpdatedAt: resp.UpdatedAt,
}, nil
}
func (rw *remoteWriter) Digest() digest.Digest {
return rw.digest
}
func (rw *remoteWriter) Write(p []byte) (n int, err error) {
offset := rw.offset
resp, err := rw.send(&contentapi.WriteRequest{
Action: contentapi.WriteActionWrite,
Offset: offset,
Data: p,
})
if err != nil {
return 0, err
}
n = int(resp.Offset - offset)
if n < len(p) {
err = io.ErrShortWrite
}
rw.offset += int64(n)
return
}
func (rw *remoteWriter) Commit(size int64, expected digest.Digest) error {
resp, err := rw.send(&contentapi.WriteRequest{
Action: contentapi.WriteActionCommit,
Total: size,
Offset: rw.offset,
Expected: expected,
})
if err != nil {
return rewriteGRPCError(err)
}
if size != 0 && resp.Offset != size {
return errors.Errorf("unexpected size: %v != %v", resp.Offset, size)
}
if expected != "" && resp.Digest != expected {
return errors.Errorf("unexpected digest: %v != %v", resp.Digest, expected)
}
return nil
}
func (rw *remoteWriter) Truncate(size int64) error {
// This truncation won't actually be validated until a write is issued.
rw.offset = size
return nil
}
func (rw *remoteWriter) Close() error {
return rw.client.CloseSend()
}

View File

@@ -0,0 +1,76 @@
package content
import (
"context"
"io"
contentapi "github.com/containerd/containerd/api/services/content"
"github.com/containerd/containerd/content"
digest "github.com/opencontainers/go-digest"
)
func NewProviderFromClient(client contentapi.ContentClient) content.Provider {
return &remoteProvider{
client: client,
}
}
type remoteProvider struct {
client contentapi.ContentClient
}
func (rp *remoteProvider) Reader(ctx context.Context, dgst digest.Digest) (io.ReadCloser, error) {
client, err := rp.client.Read(ctx, &contentapi.ReadRequest{Digest: dgst})
if err != nil {
return nil, err
}
return &remoteReader{
client: client,
}, nil
}
type remoteReader struct {
client contentapi.Content_ReadClient
extra []byte
}
func (rr *remoteReader) Read(p []byte) (n int, err error) {
n += copy(p, rr.extra)
if n >= len(p) {
if n <= len(rr.extra) {
rr.extra = rr.extra[n:]
} else {
rr.extra = rr.extra[:0]
}
return
}
p = p[n:]
for len(p) > 0 {
var resp *contentapi.ReadResponse
// fill our buffer up until we can fill p.
resp, err = rr.client.Recv()
if err != nil {
return
}
copied := copy(p, resp.Data)
n += copied
p = p[copied:]
if copied < len(p) {
continue
}
rr.extra = append(rr.extra, resp.Data[copied:]...)
}
return
}
// TODO(stevvooe): Implemente io.ReaderAt.
func (rr *remoteReader) Close() error {
return rr.client.CloseSend()
}

View File

@@ -0,0 +1,341 @@
package content
import (
"io"
"sync"
"github.com/Sirupsen/logrus"
api "github.com/containerd/containerd/api/services/content"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/plugin"
"github.com/golang/protobuf/ptypes/empty"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
type Service struct {
store *content.Store
}
var bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, 1<<20)
},
}
var _ api.ContentServer = &Service{}
func init() {
plugin.Register("content-grpc", &plugin.Registration{
Type: plugin.GRPCPlugin,
Init: NewService,
})
}
func NewService(ic *plugin.InitContext) (interface{}, error) {
return &Service{
store: ic.Content,
}, nil
}
func (s *Service) Register(server *grpc.Server) error {
api.RegisterContentServer(server, s)
return nil
}
func (s *Service) Info(ctx context.Context, req *api.InfoRequest) (*api.InfoResponse, error) {
if err := req.Digest.Validate(); err != nil {
return nil, grpc.Errorf(codes.InvalidArgument, "%q failed validation", req.Digest)
}
bi, err := s.store.Info(req.Digest)
if err != nil {
return nil, maybeNotFoundGRPC(err, req.Digest.String())
}
return &api.InfoResponse{
Digest: req.Digest,
Size_: bi.Size,
CommittedAt: bi.CommittedAt,
}, nil
}
func (s *Service) Delete(ctx context.Context, req *api.DeleteContentRequest) (*empty.Empty, error) {
if err := req.Digest.Validate(); err != nil {
return nil, grpc.Errorf(codes.InvalidArgument, err.Error())
}
if err := s.store.Delete(req.Digest); err != nil {
return nil, maybeNotFoundGRPC(err, req.Digest.String())
}
return &empty.Empty{}, nil
}
func (s *Service) Read(req *api.ReadRequest, session api.Content_ReadServer) error {
if err := req.Digest.Validate(); err != nil {
return grpc.Errorf(codes.InvalidArgument, "%v: %v", req.Digest, err)
}
oi, err := s.store.Info(req.Digest)
if err != nil {
return maybeNotFoundGRPC(err, req.Digest.String())
}
rc, err := s.store.Reader(session.Context(), req.Digest)
if err != nil {
return maybeNotFoundGRPC(err, req.Digest.String())
}
defer rc.Close() // TODO(stevvooe): Cache these file descriptors for performance.
ra, ok := rc.(io.ReaderAt)
if !ok {
// TODO(stevvooe): Need to set this up to get correct behavior across
// board. May change interface to store to just return ReaderAtCloser.
// Possibly, we could just return io.ReaderAt and handle file
// descriptors internally.
return errors.New("content service only supports content stores that return ReaderAt")
}
var (
offset = req.Offset
size = req.Size_
// TODO(stevvooe): Using the global buffer pool. At 32KB, it is probably
// little inefficient for work over a fast network. We can tune this later.
p = bufPool.Get().([]byte)
)
defer bufPool.Put(p)
if offset < 0 {
offset = 0
}
if size <= 0 {
size = oi.Size - offset
}
if offset+size > oi.Size {
return grpc.Errorf(codes.OutOfRange, "read past object length %v bytes", oi.Size)
}
if _, err := io.CopyBuffer(
&readResponseWriter{session: session},
io.NewSectionReader(ra, offset, size), p); err != nil {
return err
}
return nil
}
type readResponseWriter struct {
offset int64
session api.Content_ReadServer
}
func (rw *readResponseWriter) Write(p []byte) (n int, err error) {
if err := rw.session.Send(&api.ReadResponse{
Offset: rw.offset,
Data: p,
}); err != nil {
return 0, err
}
rw.offset += int64(len(p))
return len(p), nil
}
func (s *Service) Write(session api.Content_WriteServer) (err error) {
var (
ctx = session.Context()
msg api.WriteResponse
req *api.WriteRequest
ref string
total int64
expected digest.Digest
)
defer func(msg *api.WriteResponse) {
// pump through the last message if no error was encountered
if err != nil {
// TODO(stevvooe): Really need a log line here to track which
// errors are actually causing failure on the server side. May want
// to configure the service with an interceptor to make this work
// identically across all GRPC methods.
//
// This is pretty noisy, so we can remove it but leave it for now.
log.G(ctx).WithError(err).Error("(*Service).Write failed")
return
}
err = session.Send(msg)
}(&msg)
// handle the very first request!
req, err = session.Recv()
if err != nil {
return err
}
ref = req.Ref
if ref == "" {
return grpc.Errorf(codes.InvalidArgument, "first message must have a reference")
}
fields := logrus.Fields{
"ref": ref,
}
total = req.Total
expected = req.Expected
if total > 0 {
fields["total"] = total
}
if expected != "" {
fields["expected"] = expected
}
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(fields))
log.G(ctx).Debug("(*Service).Write started")
// this action locks the writer for the session.
wr, err := s.store.Writer(ctx, ref, total, expected)
if err != nil {
return err
}
defer wr.Close()
for {
msg.Action = req.Action
ws, err := wr.Status()
if err != nil {
return err
}
msg.Offset = ws.Offset // always set the offset.
// NOTE(stevvooe): In general, there are two cases underwhich a remote
// writer is used.
//
// For pull, we almost always have this before fetching large content,
// through descriptors. We allow predeclaration of the expected size
// and digest.
//
// For push, it is more complex. If we want to cut through content into
// storage, we may have no expectation until we are done processing the
// content. The case here is the following:
//
// 1. Start writing content.
// 2. Compress inline.
// 3. Validate digest and size (maybe).
//
// Supporting these two paths is quite awkward but it let's both API
// users use the same writer style for each with a minimum of overhead.
if req.Expected != "" {
if expected != "" && expected != req.Expected {
return grpc.Errorf(codes.InvalidArgument, "inconsistent digest provided: %v != %v", req.Expected, expected)
}
expected = req.Expected
if _, err := s.store.Info(req.Expected); err == nil {
if err := s.store.Abort(ref); err != nil {
log.G(ctx).WithError(err).Error("failed to abort write")
}
return grpc.Errorf(codes.AlreadyExists, "blob with expected digest %v exists", req.Expected)
}
}
if req.Total > 0 {
// Update the expected total. Typically, this could be seen at
// negotiation time or on a commit message.
if total > 0 && req.Total != total {
return grpc.Errorf(codes.InvalidArgument, "inconsistent total provided: %v != %v", req.Total, total)
}
total = req.Total
}
switch req.Action {
case api.WriteActionStat:
msg.Digest = wr.Digest()
msg.StartedAt = ws.StartedAt
msg.UpdatedAt = ws.UpdatedAt
msg.Total = total
case api.WriteActionWrite, api.WriteActionCommit:
if req.Offset > 0 {
// validate the offset if provided
if req.Offset != ws.Offset {
return grpc.Errorf(codes.OutOfRange, "write @%v must occur at current offset %v", req.Offset, ws.Offset)
}
}
if req.Offset == 0 && ws.Offset > 0 {
if err := wr.Truncate(req.Offset); err != nil {
return errors.Wrapf(err, "truncate failed")
}
msg.Offset = req.Offset
}
// issue the write if we actually have data.
if len(req.Data) > 0 {
// While this looks like we could use io.WriterAt here, because we
// maintain the offset as append only, we just issue the write.
n, err := wr.Write(req.Data)
if err != nil {
return err
}
if n != len(req.Data) {
// TODO(stevvooe): Perhaps, we can recover this by including it
// in the offset on the write return.
return grpc.Errorf(codes.DataLoss, "wrote %v of %v bytes", n, len(req.Data))
}
msg.Offset += int64(n)
}
if req.Action == api.WriteActionCommit {
if err := wr.Commit(total, expected); err != nil {
return err
}
msg.Digest = wr.Digest()
}
case api.WriteActionAbort:
return s.store.Abort(ref)
}
if err := session.Send(&msg); err != nil {
return err
}
req, err = session.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
return nil
}
func (s *Service) Status(*api.StatusRequest, api.Content_StatusServer) error {
return grpc.Errorf(codes.Unimplemented, "not implemented")
}
func maybeNotFoundGRPC(err error, id string) error {
if content.IsNotFound(err) {
return grpc.Errorf(codes.NotFound, "%v: not found", id)
}
return err
}

View File

@@ -0,0 +1,60 @@
package images
import (
"context"
imagesapi "github.com/containerd/containerd/api/services/images"
"github.com/containerd/containerd/images"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type remoteStore struct {
client imagesapi.ImagesClient
}
func NewStoreFromClient(client imagesapi.ImagesClient) images.Store {
return &remoteStore{
client: client,
}
}
func (s *remoteStore) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
// TODO(stevvooe): Consider that the remote may want to augment and return
// a modified image.
_, err := s.client.Put(ctx, &imagesapi.PutRequest{
Image: imagesapi.Image{
Name: name,
Target: descToProto(&desc),
},
})
return rewriteGRPCError(err)
}
func (s *remoteStore) Get(ctx context.Context, name string) (images.Image, error) {
resp, err := s.client.Get(ctx, &imagesapi.GetRequest{
Name: name,
})
if err != nil {
return images.Image{}, rewriteGRPCError(err)
}
return imageFromProto(resp.Image), nil
}
func (s *remoteStore) List(ctx context.Context) ([]images.Image, error) {
resp, err := s.client.List(ctx, &imagesapi.ListRequest{})
if err != nil {
return nil, rewriteGRPCError(err)
}
return imagesFromProto(resp.Images), nil
}
func (s *remoteStore) Delete(ctx context.Context, name string) error {
_, err := s.client.Delete(ctx, &imagesapi.DeleteRequest{
Name: name,
})
return rewriteGRPCError(err)
}

View File

@@ -0,0 +1,87 @@
package images
import (
imagesapi "github.com/containerd/containerd/api/services/images"
"github.com/containerd/containerd/api/types/descriptor"
"github.com/containerd/containerd/images"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
func imagesToProto(images []images.Image) []imagesapi.Image {
var imagespb []imagesapi.Image
for _, image := range images {
imagespb = append(imagespb, imageToProto(&image))
}
return imagespb
}
func imagesFromProto(imagespb []imagesapi.Image) []images.Image {
var images []images.Image
for _, image := range imagespb {
images = append(images, imageFromProto(&image))
}
return images
}
func imageToProto(image *images.Image) imagesapi.Image {
return imagesapi.Image{
Name: image.Name,
Target: descToProto(&image.Target),
}
}
func imageFromProto(imagepb *imagesapi.Image) images.Image {
return images.Image{
Name: imagepb.Name,
Target: descFromProto(&imagepb.Target),
}
}
func descFromProto(desc *descriptor.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: desc.MediaType,
Size: desc.Size_,
Digest: desc.Digest,
}
}
func descToProto(desc *ocispec.Descriptor) descriptor.Descriptor {
return descriptor.Descriptor{
MediaType: desc.MediaType,
Size_: desc.Size,
Digest: desc.Digest,
}
}
func rewriteGRPCError(err error) error {
if err == nil {
return err
}
switch grpc.Code(errors.Cause(err)) {
case codes.AlreadyExists:
return images.ErrExists
case codes.NotFound:
return images.ErrNotFound
}
return err
}
func mapGRPCError(err error, id string) error {
switch {
case images.IsNotFound(err):
return grpc.Errorf(codes.NotFound, "image %v not found", id)
case images.IsExists(err):
return grpc.Errorf(codes.AlreadyExists, "image %v already exists", id)
}
return err
}

View File

@@ -0,0 +1,85 @@
package images
import (
"github.com/boltdb/bolt"
imagesapi "github.com/containerd/containerd/api/services/images"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/plugin"
"github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
func init() {
plugin.Register("images-grpc", &plugin.Registration{
Type: plugin.GRPCPlugin,
Init: func(ic *plugin.InitContext) (interface{}, error) {
return NewService(ic.Meta), nil
},
})
}
type Service struct {
db *bolt.DB
}
func NewService(db *bolt.DB) imagesapi.ImagesServer {
return &Service{db: db}
}
func (s *Service) Register(server *grpc.Server) error {
imagesapi.RegisterImagesServer(server, s)
return nil
}
func (s *Service) Get(ctx context.Context, req *imagesapi.GetRequest) (*imagesapi.GetResponse, error) {
var resp imagesapi.GetResponse
return &resp, s.withStoreView(ctx, func(ctx context.Context, store images.Store) error {
image, err := store.Get(ctx, req.Name)
if err != nil {
return mapGRPCError(err, req.Name)
}
imagepb := imageToProto(&image)
resp.Image = &imagepb
return nil
})
}
func (s *Service) Put(ctx context.Context, req *imagesapi.PutRequest) (*empty.Empty, error) {
return &empty.Empty{}, s.withStoreUpdate(ctx, func(ctx context.Context, store images.Store) error {
return mapGRPCError(store.Put(ctx, req.Image.Name, descFromProto(&req.Image.Target)), req.Image.Name)
})
}
func (s *Service) List(ctx context.Context, _ *imagesapi.ListRequest) (*imagesapi.ListResponse, error) {
var resp imagesapi.ListResponse
return &resp, s.withStoreView(ctx, func(ctx context.Context, store images.Store) error {
images, err := store.List(ctx)
if err != nil {
return mapGRPCError(err, "")
}
resp.Images = imagesToProto(images)
return nil
})
}
func (s *Service) Delete(ctx context.Context, req *imagesapi.DeleteRequest) (*empty.Empty, error) {
return &empty.Empty{}, s.withStoreUpdate(ctx, func(ctx context.Context, store images.Store) error {
return mapGRPCError(store.Delete(ctx, req.Name), req.Name)
})
}
func (s *Service) withStore(ctx context.Context, fn func(ctx context.Context, store images.Store) error) func(tx *bolt.Tx) error {
return func(tx *bolt.Tx) error { return fn(ctx, images.NewImageStore(tx)) }
}
func (s *Service) withStoreView(ctx context.Context, fn func(ctx context.Context, store images.Store) error) error {
return s.db.View(s.withStore(ctx, fn))
}
func (s *Service) withStoreUpdate(ctx context.Context, fn func(ctx context.Context, store images.Store) error) error {
return s.db.Update(s.withStore(ctx, fn))
}

View File

@@ -0,0 +1,39 @@
package rootfs
import (
"context"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
containerd_v1_types "github.com/containerd/containerd/api/types/descriptor"
"github.com/containerd/containerd/rootfs"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func NewUnpackerFromClient(client rootfsapi.RootFSClient) rootfs.Unpacker {
return remoteUnpacker{
client: client,
}
}
type remoteUnpacker struct {
client rootfsapi.RootFSClient
}
func (rp remoteUnpacker) Unpack(ctx context.Context, layers []ocispec.Descriptor) (digest.Digest, error) {
pr := rootfsapi.UnpackRequest{
Layers: make([]*containerd_v1_types.Descriptor, len(layers)),
}
for i, l := range layers {
pr.Layers[i] = &containerd_v1_types.Descriptor{
MediaType: l.MediaType,
Digest: l.Digest,
Size_: l.Size,
}
}
resp, err := rp.client.Unpack(ctx, &pr)
if err != nil {
return "", err
}
return resp.ChainID, nil
}

View File

@@ -0,0 +1,114 @@
package rootfs
import (
"github.com/containerd/containerd"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
containerd_v1_types "github.com/containerd/containerd/api/types/mount"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/rootfs"
"github.com/containerd/containerd/snapshot"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
func init() {
plugin.Register("rootfs-grpc", &plugin.Registration{
Type: plugin.GRPCPlugin,
Init: func(ic *plugin.InitContext) (interface{}, error) {
return NewService(ic.Content, ic.Snapshotter)
},
})
}
type Service struct {
store *content.Store
snapshotter snapshot.Snapshotter
}
func NewService(store *content.Store, snapshotter snapshot.Snapshotter) (*Service, error) {
return &Service{
store: store,
snapshotter: snapshotter,
}, nil
}
func (s *Service) Register(gs *grpc.Server) error {
rootfsapi.RegisterRootFSServer(gs, s)
return nil
}
func (s *Service) Unpack(ctx context.Context, pr *rootfsapi.UnpackRequest) (*rootfsapi.UnpackResponse, error) {
layers := make([]ocispec.Descriptor, len(pr.Layers))
for i, l := range pr.Layers {
layers[i] = ocispec.Descriptor{
MediaType: l.MediaType,
Digest: l.Digest,
Size: l.Size_,
}
}
log.G(ctx).Infof("Preparing %#v", layers)
chainID, err := rootfs.Prepare(ctx, s.snapshotter, mounter{}, layers, s.store.Reader, emptyResolver, noopRegister)
if err != nil {
log.G(ctx).Errorf("Rootfs Prepare failed!: %v", err)
return nil, err
}
log.G(ctx).Infof("ChainID %#v", chainID)
return &rootfsapi.UnpackResponse{
ChainID: chainID,
}, nil
}
func (s *Service) Prepare(ctx context.Context, ir *rootfsapi.PrepareRequest) (*rootfsapi.MountResponse, error) {
mounts, err := rootfs.InitRootFS(ctx, ir.Name, ir.ChainID, ir.Readonly, s.snapshotter, mounter{})
if err != nil {
return nil, grpc.Errorf(codes.AlreadyExists, "%v", err)
}
return &rootfsapi.MountResponse{
Mounts: apiMounts(mounts),
}, nil
}
func (s *Service) Mounts(ctx context.Context, mr *rootfsapi.MountsRequest) (*rootfsapi.MountResponse, error) {
mounts, err := s.snapshotter.Mounts(ctx, mr.Name)
if err != nil {
return nil, err
}
return &rootfsapi.MountResponse{
Mounts: apiMounts(mounts),
}, nil
}
func apiMounts(mounts []containerd.Mount) []*containerd_v1_types.Mount {
am := make([]*containerd_v1_types.Mount, len(mounts))
for i, m := range mounts {
am[i] = &containerd_v1_types.Mount{
Type: m.Type,
Source: m.Source,
Options: m.Options,
}
}
return am
}
type mounter struct{}
func (mounter) Mount(dir string, mounts ...containerd.Mount) error {
return containerd.MountAll(mounts, dir)
}
func (mounter) Unmount(dir string) error {
return containerd.Unmount(dir, 0)
}
func emptyResolver(digest.Digest) digest.Digest {
return digest.Digest("")
}
func noopRegister(digest.Digest, digest.Digest) error {
return nil
}

View File

@@ -0,0 +1,44 @@
package snapshot
import "github.com/pkg/errors"
var (
// ErrSnapshotNotExist is returned when a snapshot cannot be found
ErrSnapshotNotExist = errors.New("snapshot does not exist")
// ErrSnapshotExist is returned when an operation to create a snapshot
// encounters a snapshot with the same key
ErrSnapshotExist = errors.New("snapshot already exists")
// ErrSnapshotNotActive is returned when a request which requires an
// active snapshot encounters a non-active snapshot.
ErrSnapshotNotActive = errors.New("snapshot is not active")
// ErrSnapshotNotCommitted is returned when a request which requires a
// committed snapshot encounters a non-committed snapshot.
ErrSnapshotNotCommitted = errors.New("snapshot is not committed")
)
// IsNotExist returns whether the error represents that a snapshot
// was not found.
func IsNotExist(err error) bool {
return errors.Cause(err) == ErrSnapshotNotExist
}
// IsExist returns whether the error represents whether a snapshot
// already exists using a provided key.
func IsExist(err error) bool {
return errors.Cause(err) == ErrSnapshotExist
}
// IsNotActive returns whether the error represents a request
// for a non active snapshot when an active snapshot is expected.
func IsNotActive(err error) bool {
return errors.Cause(err) == ErrSnapshotNotActive
}
// IsNotCommitted returns whether the error represents a request
// for a non committed snapshot when a committed snapshot is expected.
func IsNotCommitted(err error) bool {
return errors.Cause(err) == ErrSnapshotNotCommitted
}

View File

@@ -0,0 +1,209 @@
package snapshot
import (
"context"
"github.com/containerd/containerd"
)
// Kind identifies the kind of snapshot.
type Kind int
// definitions of snapshot kinds
const (
KindActive Kind = iota
KindCommitted
)
// Info provides information about a particular snapshot.
type Info struct {
Name string // name or key of snapshot
Parent string // name of parent snapshot
Kind Kind // active or committed snapshot
Readonly bool // true if readonly, only valid for active
}
// Snapshotter defines the methods required to implement a snapshot snapshotter for
// allocating, snapshotting and mounting filesystem changesets. The model works
// by building up sets of changes with parent-child relationships.
//
// A snapshot represents a filesystem state. Every snapshot has a parent, where
// the empty parent is represented by the empty string. A diff can be taken
// between a parent and its snapshot to generate a classic layer.
//
// An active snapshot is created by calling `Prepare`. After mounting, changes
// can be made to the snapshot. The act of commiting creates a committed
// snapshot. The committed snapshot will get the parent of active snapshot. The
// committed snapshot can then be used as a parent. Active snapshots can never
// act as a parent.
//
// Snapshots are best understood by their lifecycle. Active snapshots are
// always created with Prepare or View. Committed snapshots are always created
// with Commit. Active snapshots never become committed snapshots and vice
// versa. All snapshots may be removed.
//
// For consistency, we define the following terms to be used throughout this
// interface for snapshotter implementations:
//
// `key` - refers to an active snapshot
// `name` - refers to a committed snapshot
// `parent` - refers to the parent in relation
//
// Most methods take various combinations of these identifiers. Typically,
// `name` and `parent` will be used in cases where a method *only* takes
// committed snapshots. `key` will be used to refer to active snapshots in most
// cases, except where noted. All variables used to access snapshots use the
// same key space. For example, an active snapshot may not share the same key
// with a committed snapshot.
//
// We cover several examples below to demonstrate the utility of a snapshot
// snapshotter.
//
// Importing a Layer
//
// To import a layer, we simply have the Snapshotter provide a list of
// mounts to be applied such that our dst will capture a changeset. We start
// out by getting a path to the layer tar file and creating a temp location to
// unpack it to:
//
// layerPath, tmpDir := getLayerPath(), mkTmpDir() // just a path to layer tar file.
//
// We start by using a Snapshotter to Prepare a new snapshot transaction, using a
// key and descending from the empty parent "":
//
// mounts, err := snapshotter.Prepare(key, "")
// if err != nil { ... }
//
// We get back a list of mounts from Snapshotter.Prepare, with the key identifying
// the active snapshot. Mount this to the temporary location with the
// following:
//
// if err := MountAll(mounts, tmpDir); err != nil { ... }
//
// Once the mounts are performed, our temporary location is ready to capture
// a diff. In practice, this works similar to a filesystem transaction. The
// next step is to unpack the layer. We have a special function unpackLayer
// that applies the contents of the layer to target location and calculates the
// DiffID of the unpacked layer (this is a requirement for docker
// implementation):
//
// layer, err := os.Open(layerPath)
// if err != nil { ... }
// digest, err := unpackLayer(tmpLocation, layer) // unpack into layer location
// if err != nil { ... }
//
// When the above completes, we should have a filesystem the represents the
// contents of the layer. Careful implementations should verify that digest
// matches the expected DiffID. When completed, we unmount the mounts:
//
// unmount(mounts) // optional, for now
//
// Now that we've verified and unpacked our layer, we commit the active
// snapshot to a name. For this example, we are just going to use the layer
// digest, but in practice, this will probably be the ChainID:
//
// if err := snapshotter.Commit(digest.String(), key); err != nil { ... }
//
// Now, we have a layer in the Snapshotter that can be accessed with the digest
// provided during commit. Once you have committed the snapshot, the active
// snapshot can be removed with the following:
//
// snapshotter.Remove(key)
//
// Importing the Next Layer
//
// Making a layer depend on the above is identical to the process described
// above except that the parent is provided as parent when calling
// Manager.Prepare, assuming a clean tmpLocation:
//
// mounts, err := snapshotter.Prepare(tmpLocation, parentDigest)
//
// We then mount, apply and commit, as we did above. The new snapshot will be
// based on the content of the previous one.
//
// Running a Container
//
// To run a container, we simply provide Snapshotter.Prepare the committed image
// snapshot as the parent. After mounting, the prepared path can
// be used directly as the container's filesystem:
//
// mounts, err := snapshotter.Prepare(containerKey, imageRootFSChainID)
//
// The returned mounts can then be passed directly to the container runtime. If
// one would like to create a new image from the filesystem, Manager.Commit is
// called:
//
// if err := snapshotter.Commit(newImageSnapshot, containerKey); err != nil { ... }
//
// Alternatively, for most container runs, Snapshotter.Remove will be called to
// signal the Snapshotter to abandon the changes.
type Snapshotter interface {
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
Stat(ctx context.Context, key string) (Info, error)
// Mounts returns the mounts for the active snapshot transaction identified
// by key. Can be called on an read-write or readonly transaction. This is
// available only for active snapshots.
//
// This can be used to recover mounts after calling View or Prepare.
Mounts(ctx context.Context, key string) ([]containerd.Mount, error)
// Prepare creates an active snapshot identified by key descending from the
// provided parent. The returned mounts can be used to mount the snapshot
// to capture changes.
//
// If a parent is provided, after performing the mounts, the destination
// will start with the content of the parent. The parent must be a
// committed snapshot. Changes to the mounted destination will be captured
// in relation to the parent. The default parent, "", is an empty
// directory.
//
// The changes may be saved to a committed snapshot by calling Commit. When
// one is done with the transaction, Remove should be called on the key.
//
// Multiple calls to Prepare or View with the same key should fail.
Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error)
// View behaves identically to Prepare except the result may not be
// committed back to the snapshot snapshotter. View returns a readonly view on
// the parent, with the active snapshot being tracked by the given key.
//
// This method operates identically to Prepare, except that Mounts returned
// may have the readonly flag set. Any modifications to the underlying
// filesystem will be ignored. Implementations may perform this in a more
// efficient manner that differs from what would be attempted with
// `Prepare`.
//
// Commit may not be called on the provided key and will return an error.
// To collect the resources associated with key, Remove must be called with
// key as the argument.
View(ctx context.Context, key, parent string) ([]containerd.Mount, error)
// Commit captures the changes between key and its parent into a snapshot
// identified by name. The name can then be used with the snapshotter's other
// methods to create subsequent snapshots.
//
// A committed snapshot will be created under name with the parent of the
// active snapshot.
//
// Commit may be called multiple times on the same key. Snapshots created
// in this manner will all reference the parent used to start the
// transaction.
Commit(ctx context.Context, name, key string) error
// Remove the committed or active snapshot by the provided key.
//
// All resources associated with the key will be removed.
//
// If the snapshot is a parent of another snapshot, its children must be
// removed before proceeding.
Remove(ctx context.Context, key string) error
// Walk the committed snapshots. For each snapshot in the snapshotter, the
// function will be called.
Walk(ctx context.Context, fn func(context.Context, Info) error) error
}

20
vendor/github.com/containerd/containerd/sys/epoll.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// +build linux,!arm64
package sys
import "golang.org/x/sys/unix"
// EpollCreate1 directly calls unix.EpollCreate1
func EpollCreate1(flag int) (int, error) {
return unix.EpollCreate1(flag)
}
// EpollCtl directly calls unix.EpollCtl
func EpollCtl(epfd int, op int, fd int, event *unix.EpollEvent) error {
return unix.EpollCtl(epfd, op, fd, event)
}
// EpollWait directly calls unix.EpollWait
func EpollWait(epfd int, events []unix.EpollEvent, msec int) (int, error) {
return unix.EpollWait(epfd, events, msec)
}

View File

@@ -0,0 +1,74 @@
// +build linux,arm64
package sys
// #include <sys/epoll.h>
/*
int EpollCreate1(int flag) {
return epoll_create1(flag);
}
int EpollCtl(int efd, int op,int sfd, int events, int fd) {
struct epoll_event event;
event.events = events;
event.data.fd = fd;
return epoll_ctl(efd, op, sfd, &event);
}
struct event_t {
uint32_t events;
int fd;
};
struct epoll_event events[128];
int run_epoll_wait(int fd, struct event_t *event) {
int n, i;
n = epoll_wait(fd, events, 128, -1);
for (i = 0; i < n; i++) {
event[i].events = events[i].events;
event[i].fd = events[i].data.fd;
}
return n;
}
*/
import "C"
import (
"fmt"
"unsafe"
"golang.org/x/sys/unix"
)
// EpollCreate1 calls a C implementation
func EpollCreate1(flag int) (int, error) {
fd := int(C.EpollCreate1(C.int(flag)))
if fd < 0 {
return fd, fmt.Errorf("failed to create epoll, errno is %d", fd)
}
return fd, nil
}
// EpollCtl calls a C implementation
func EpollCtl(epfd int, op int, fd int, event *unix.EpollEvent) error {
errno := C.EpollCtl(C.int(epfd), C.int(unix.EPOLL_CTL_ADD), C.int(fd), C.int(event.Events), C.int(event.Fd))
if errno < 0 {
return fmt.Errorf("Failed to ctl epoll")
}
return nil
}
// EpollWait calls a C implementation
func EpollWait(epfd int, events []unix.EpollEvent, msec int) (int, error) {
var c_events [128]C.struct_event_t
n := int(C.run_epoll_wait(C.int(epfd), (*C.struct_event_t)(unsafe.Pointer(&c_events))))
if n < 0 {
return int(n), fmt.Errorf("Failed to wait epoll")
}
for i := 0; i < n; i++ {
events[i].Fd = int32(c_events[i].fd)
events[i].Events = uint32(c_events[i].events)
}
return int(n), nil
}

18
vendor/github.com/containerd/containerd/sys/fds.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
// +build !windows,!darwin
package sys
import (
"io/ioutil"
"path/filepath"
"strconv"
)
// GetOpenFds returns the number of open fds for the process provided by pid
func GetOpenFds(pid int) (int, error) {
dirs, err := ioutil.ReadDir(filepath.Join("/proc", strconv.Itoa(pid), "fd"))
if err != nil {
return -1, err
}
return len(dirs), nil
}

View File

@@ -0,0 +1,236 @@
// +build windows
package sys
import (
"os"
"path/filepath"
"regexp"
"strings"
"syscall"
"unsafe"
winio "github.com/Microsoft/go-winio"
)
// MkdirAllWithACL is a wrapper for MkdirAll that creates a directory
// ACL'd for Builtin Administrators and Local System.
func MkdirAllWithACL(path string, perm os.FileMode) error {
return mkdirall(path, true)
}
// MkdirAll implementation that is volume path aware for Windows.
func MkdirAll(path string, _ os.FileMode) error {
return mkdirall(path, false)
}
// mkdirall is a custom version of os.MkdirAll modified for use on Windows
// so that it is both volume path aware, and can create a directory with
// a DACL.
func mkdirall(path string, adminAndLocalSystem bool) error {
if re := regexp.MustCompile(`^\\\\\?\\Volume{[a-z0-9-]+}$`); re.MatchString(path) {
return nil
}
// The rest of this method is largely copied from os.MkdirAll and should be kept
// as-is to ensure compatibility.
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
dir, err := os.Stat(path)
if err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{
Op: "mkdir",
Path: path,
Err: syscall.ENOTDIR,
}
}
// Slow path: make sure parent exists and then call Mkdir for path.
i := len(path)
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
i--
}
j := i
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
j--
}
if j > 1 {
// Create parent
err = mkdirall(path[0:j-1], false)
if err != nil {
return err
}
}
// Parent now exists; invoke os.Mkdir or mkdirWithACL and use its result.
if adminAndLocalSystem {
err = mkdirWithACL(path)
} else {
err = os.Mkdir(path, 0)
}
if err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := os.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}
return nil
}
// mkdirWithACL creates a new directory. If there is an error, it will be of
// type *PathError. .
//
// This is a modified and combined version of os.Mkdir and syscall.Mkdir
// in golang to cater for creating a directory am ACL permitting full
// access, with inheritance, to any subfolder/file for Built-in Administrators
// and Local System.
func mkdirWithACL(name string) error {
sa := syscall.SecurityAttributes{Length: 0}
sddl := "D:P(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)"
sd, err := winio.SddlToSecurityDescriptor(sddl)
if err != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: err}
}
sa.Length = uint32(unsafe.Sizeof(sa))
sa.InheritHandle = 1
sa.SecurityDescriptor = uintptr(unsafe.Pointer(&sd[0]))
namep, err := syscall.UTF16PtrFromString(name)
if err != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: err}
}
e := syscall.CreateDirectory(namep, &sa)
if e != nil {
return &os.PathError{Op: "mkdir", Path: name, Err: e}
}
return nil
}
// IsAbs is a platform-specific wrapper for filepath.IsAbs. On Windows,
// golang filepath.IsAbs does not consider a path \windows\system32 as absolute
// as it doesn't start with a drive-letter/colon combination. However, in
// docker we need to verify things such as WORKDIR /windows/system32 in
// a Dockerfile (which gets translated to \windows\system32 when being processed
// by the daemon. This SHOULD be treated as absolute from a docker processing
// perspective.
func IsAbs(path string) bool {
if !filepath.IsAbs(path) {
if !strings.HasPrefix(path, string(os.PathSeparator)) {
return false
}
}
return true
}
// The origin of the functions below here are the golang OS and syscall packages,
// slightly modified to only cope with files, not directories due to the
// specific use case.
//
// The alteration is to allow a file on Windows to be opened with
// FILE_FLAG_SEQUENTIAL_SCAN (particular for docker load), to avoid eating
// the standby list, particularly when accessing large files such as layer.tar.
// CreateSequential creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned
// File can be used for I/O; the associated file descriptor has mode
// O_RDWR.
// If there is an error, it will be of type *PathError.
func CreateSequential(name string) (*os.File, error) {
return OpenFileSequential(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0)
}
// OpenSequential opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func OpenSequential(name string) (*os.File, error) {
return OpenFileSequential(name, os.O_RDONLY, 0)
}
// OpenFileSequential is the generalized open call; most users will use Open
// or Create instead.
// If there is an error, it will be of type *PathError.
func OpenFileSequential(name string, flag int, _ os.FileMode) (*os.File, error) {
if name == "" {
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOENT}
}
r, errf := syscallOpenFileSequential(name, flag, 0)
if errf == nil {
return r, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: errf}
}
func syscallOpenFileSequential(name string, flag int, _ os.FileMode) (file *os.File, err error) {
r, e := syscallOpenSequential(name, flag|syscall.O_CLOEXEC, 0)
if e != nil {
return nil, e
}
return os.NewFile(uintptr(r), name), nil
}
func makeInheritSa() *syscall.SecurityAttributes {
var sa syscall.SecurityAttributes
sa.Length = uint32(unsafe.Sizeof(sa))
sa.InheritHandle = 1
return &sa
}
func syscallOpenSequential(path string, mode int, _ uint32) (fd syscall.Handle, err error) {
if len(path) == 0 {
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.InvalidHandle, err
}
var access uint32
switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
case syscall.O_RDONLY:
access = syscall.GENERIC_READ
case syscall.O_WRONLY:
access = syscall.GENERIC_WRITE
case syscall.O_RDWR:
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
}
if mode&syscall.O_CREAT != 0 {
access |= syscall.GENERIC_WRITE
}
if mode&syscall.O_APPEND != 0 {
access &^= syscall.GENERIC_WRITE
access |= syscall.FILE_APPEND_DATA
}
sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE)
var sa *syscall.SecurityAttributes
if mode&syscall.O_CLOEXEC == 0 {
sa = makeInheritSa()
}
var createmode uint32
switch {
case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
createmode = syscall.CREATE_NEW
case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC):
createmode = syscall.CREATE_ALWAYS
case mode&syscall.O_CREAT == syscall.O_CREAT:
createmode = syscall.OPEN_ALWAYS
case mode&syscall.O_TRUNC == syscall.O_TRUNC:
createmode = syscall.TRUNCATE_EXISTING
default:
createmode = syscall.OPEN_EXISTING
}
// Use FILE_FLAG_SEQUENTIAL_SCAN rather than FILE_ATTRIBUTE_NORMAL as implemented in golang.
//https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
const fileFlagSequentialScan = 0x08000000 // FILE_FLAG_SEQUENTIAL_SCAN
h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, fileFlagSequentialScan, 0)
return h, e
}

View File

@@ -0,0 +1,31 @@
// +build !windows
package sys
import (
"fmt"
"os"
"strconv"
"github.com/opencontainers/runc/libcontainer/system"
)
// OOMScoreMaxKillable is the maximum score keeping the process killable by the oom killer
const OOMScoreMaxKillable = -999
// SetOOMScore sets the oom score for the provided pid
func SetOOMScore(pid, score int) error {
path := fmt.Sprintf("/proc/%d/oom_score_adj", pid)
f, err := os.OpenFile(path, os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()
if _, err = f.WriteString(strconv.Itoa(score)); err != nil {
if os.IsPermission(err) && system.RunningInUserNS() {
return nil
}
return err
}
return nil
}

View File

@@ -0,0 +1,5 @@
package sys
func SetOOMScore(pid, score int) error {
return nil
}

49
vendor/github.com/containerd/containerd/sys/prctl.go generated vendored Normal file
View File

@@ -0,0 +1,49 @@
// +build linux
// Package osutils provide access to the Get Child and Set Child prctl
// flags.
// See http://man7.org/linux/man-pages/man2/prctl.2.html
package sys
import (
"unsafe"
"golang.org/x/sys/unix"
)
// PR_SET_CHILD_SUBREAPER allows setting the child subreaper.
// If arg2 is nonzero, set the "child subreaper" attribute of the
// calling process; if arg2 is zero, unset the attribute. When a
// process is marked as a child subreaper, all of the children
// that it creates, and their descendants, will be marked as
// having a subreaper. In effect, a subreaper fulfills the role
// of init(1) for its descendant processes. Upon termination of
// a process that is orphaned (i.e., its immediate parent has
// already terminated) and marked as having a subreaper, the
// nearest still living ancestor subreaper will receive a SIGCHLD
// signal and be able to wait(2) on the process to discover its
// termination status.
const prSetChildSubreaper = 36
// PR_GET_CHILD_SUBREAPER allows retrieving the current child
// subreaper.
// Returns the "child subreaper" setting of the caller, in the
// location pointed to by (int *) arg2.
const prGetChildSubreaper = 37
// GetSubreaper returns the subreaper setting for the calling process
func GetSubreaper() (int, error) {
var i uintptr
if _, _, err := unix.RawSyscall(unix.SYS_PRCTL, prGetChildSubreaper, uintptr(unsafe.Pointer(&i)), 0); err != 0 {
return -1, err
}
return int(i), nil
}
// SetSubreaper sets the value i as the subreaper setting for the calling process
func SetSubreaper(i int) error {
if _, _, err := unix.RawSyscall(unix.SYS_PRCTL, prSetChildSubreaper, uintptr(i), 0); err != 0 {
return err
}
return nil
}

View File

@@ -0,0 +1,19 @@
// +build solaris
package sys
import (
"errors"
)
//Solaris TODO
// GetSubreaper returns the subreaper setting for the calling process
func GetSubreaper() (int, error) {
return 0, errors.New("osutils GetSubreaper not implemented on Solaris")
}
// SetSubreaper sets the value i as the subreaper setting for the calling process
func SetSubreaper(i int) error {
return errors.New("osutils SetSubreaper not implemented on Solaris")
}

64
vendor/github.com/containerd/containerd/sys/proc.go generated vendored Normal file
View File

@@ -0,0 +1,64 @@
// +build linux
package sys
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/opencontainers/runc/libcontainer/system"
)
const nanoSecondsPerSecond = 1e9
var clockTicksPerSecond = uint64(system.GetClockTicks())
// GetSystemCPUUsage returns the host system's cpu usage in
// nanoseconds. An error is returned if the format of the underlying
// file does not match.
//
// Uses /proc/stat defined by POSIX. Looks for the cpu
// statistics line and then sums up the first seven fields
// provided. See `man 5 proc` for details on specific field
// information.
func GetSystemCPUUsage() (uint64, error) {
var line string
f, err := os.Open("/proc/stat")
if err != nil {
return 0, err
}
bufReader := bufio.NewReaderSize(nil, 128)
defer func() {
bufReader.Reset(nil)
f.Close()
}()
bufReader.Reset(f)
err = nil
for err == nil {
line, err = bufReader.ReadString('\n')
if err != nil {
break
}
parts := strings.Fields(line)
switch parts[0] {
case "cpu":
if len(parts) < 8 {
return 0, fmt.Errorf("bad format of cpu stats")
}
var totalClockTicks uint64
for _, i := range parts[1:8] {
v, err := strconv.ParseUint(i, 10, 64)
if err != nil {
return 0, fmt.Errorf("error parsing cpu stats")
}
totalClockTicks += v
}
return (totalClockTicks * nanoSecondsPerSecond) /
clockTicksPerSecond, nil
}
}
return 0, fmt.Errorf("bad stats format")
}

51
vendor/github.com/containerd/containerd/sys/reaper.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
// +build !windows
package sys
import "golang.org/x/sys/unix"
// Exit is the wait4 information from an exited process
type Exit struct {
Pid int
Status int
}
// Reap reaps all child processes for the calling process and returns their
// exit information
func Reap(wait bool) (exits []Exit, err error) {
var (
ws unix.WaitStatus
rus unix.Rusage
)
flag := unix.WNOHANG
if wait {
flag = 0
}
for {
pid, err := unix.Wait4(-1, &ws, flag, &rus)
if err != nil {
if err == unix.ECHILD {
return exits, nil
}
return exits, err
}
if pid <= 0 {
return exits, nil
}
exits = append(exits, Exit{
Pid: pid,
Status: exitStatus(ws),
})
}
}
const exitSignalOffset = 128
// exitStatus returns the correct exit status for a process based on if it
// was signaled or exited cleanly
func exitStatus(status unix.WaitStatus) int {
if status.Signaled() {
return exitSignalOffset + int(status.Signal())
}
return status.ExitStatus()
}

View File

@@ -0,0 +1,37 @@
// +build !windows
package sys
import (
"net"
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
// CreateUnixSocket creates a unix socket and returns the listener
func CreateUnixSocket(path string) (net.Listener, error) {
if err := os.MkdirAll(filepath.Dir(path), 0660); err != nil {
return nil, err
}
if err := unix.Unlink(path); err != nil && !os.IsNotExist(err) {
return nil, err
}
return net.Listen("unix", path)
}
// GetLocalListener returns a listerner out of a unix socket.
func GetLocalListener(path string, uid, gid int) (net.Listener, error) {
l, err := CreateUnixSocket(path)
if err != nil {
return l, err
}
if err := os.Chown(path, uid, gid); err != nil {
l.Close()
return nil, err
}
return l, nil
}

View File

@@ -0,0 +1,16 @@
// +build windows
package sys
import (
"net"
"github.com/Microsoft/go-winio"
)
// GetLocalListener returns a Listernet out of a named pipe.
// `path` must be of the form of `\\.\pipe\<pipename>`
// (see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365150)
func GetLocalListener(path string, uid, gid int) (net.Listener, error) {
return winio.ListenPipe(path, nil)
}