diff --git a/api/server/httputils/form.go b/api/server/httputils/form.go index a5f62287ec..1ce822ed94 100644 --- a/api/server/httputils/form.go +++ b/api/server/httputils/form.go @@ -2,7 +2,6 @@ package httputils import ( "net/http" - "path/filepath" "strconv" "strings" ) @@ -69,8 +68,7 @@ func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions, if name == "" { return ArchiveOptions{}, badParameterError{"name"} } - - path := filepath.FromSlash(r.Form.Get("path")) + path := r.Form.Get("path") if path == "" { return ArchiveOptions{}, badParameterError{"path"} } diff --git a/builder/builder.go b/builder/builder.go index e480601d46..f376f530cd 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types/container" containerpkg "github.com/docker/docker/container" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/containerfs" "golang.org/x/net/context" ) @@ -24,7 +25,7 @@ const ( // instructions in the builder. type Source interface { // Root returns root path for accessing source - Root() string + Root() containerfs.ContainerFS // Close allows to signal that the filesystem tree won't be used anymore. // For Context implementations using a temporary directory, it is recommended to // delete the temporary directory in Close(). @@ -99,7 +100,7 @@ type Image interface { // ReleaseableLayer is an image layer that can be mounted and released type ReleaseableLayer interface { Release() error - Mount() (string, error) + Mount() (containerfs.ContainerFS, error) Commit(platform string) (ReleaseableLayer, error) DiffID() layer.DiffID } diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index 34db3786f1..46a5af7395 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -17,8 +17,6 @@ import ( "github.com/docker/docker/builder/dockerfile/parser" "github.com/docker/docker/builder/fscache" "github.com/docker/docker/builder/remotecontext" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" @@ -50,21 +48,21 @@ type SessionGetter interface { // BuildManager is shared across all Builder objects type BuildManager struct { - archiver *archive.Archiver - backend builder.Backend - pathCache pathCache // TODO: make this persistent - sg SessionGetter - fsCache *fscache.FSCache + idMappings *idtools.IDMappings + backend builder.Backend + pathCache pathCache // TODO: make this persistent + sg SessionGetter + fsCache *fscache.FSCache } // NewBuildManager creates a BuildManager func NewBuildManager(b builder.Backend, sg SessionGetter, fsCache *fscache.FSCache, idMappings *idtools.IDMappings) (*BuildManager, error) { bm := &BuildManager{ - backend: b, - pathCache: &syncmap.Map{}, - sg: sg, - archiver: chrootarchive.NewArchiver(idMappings), - fsCache: fsCache, + backend: b, + pathCache: &syncmap.Map{}, + sg: sg, + idMappings: idMappings, + fsCache: fsCache, } if err := fsCache.RegisterTransport(remotecontext.ClientSessionRemote, NewClientSessionTransport()); err != nil { return nil, err @@ -114,7 +112,7 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) ( ProgressWriter: config.ProgressWriter, Backend: bm.backend, PathCache: bm.pathCache, - Archiver: bm.archiver, + IDMappings: bm.idMappings, Platform: dockerfile.Platform, } @@ -160,7 +158,7 @@ type builderOptions struct { Backend builder.Backend ProgressWriter backend.ProgressWriter PathCache pathCache - Archiver *archive.Archiver + IDMappings *idtools.IDMappings Platform string } @@ -177,7 +175,7 @@ type Builder struct { docker builder.Backend clientCtx context.Context - archiver *archive.Archiver + idMappings *idtools.IDMappings buildStages *buildStages disableCommit bool buildArgs *buildArgs @@ -219,7 +217,7 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder { Aux: options.ProgressWriter.AuxFormatter, Output: options.ProgressWriter.Output, docker: options.Backend, - archiver: options.Archiver, + idMappings: options.IDMappings, buildArgs: newBuildArgs(config.BuildArgs), buildStages: newBuildStages(), imageSources: newImageSources(clientCtx, options), diff --git a/builder/dockerfile/copy.go b/builder/dockerfile/copy.go index 3de63d542e..f4b703d8d1 100644 --- a/builder/dockerfile/copy.go +++ b/builder/dockerfile/copy.go @@ -1,6 +1,7 @@ package dockerfile import ( + "archive/tar" "fmt" "io" "mime" @@ -8,6 +9,7 @@ import ( "net/url" "os" "path/filepath" + "runtime" "sort" "strings" "time" @@ -15,11 +17,11 @@ import ( "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/urlutil" "github.com/pkg/errors" @@ -35,14 +37,14 @@ type pathCache interface { // copyInfo is a data object which stores the metadata about each source file in // a copyInstruction type copyInfo struct { - root string + root containerfs.ContainerFS path string hash string noDecompress bool } func (c copyInfo) fullPath() (string, error) { - return symlink.FollowSymlinkInScope(filepath.Join(c.root, c.path), c.root) + return c.root.ResolveScopedPath(c.path, true) } func newCopyInfoFromSource(source builder.Source, path string, hash string) copyInfo { @@ -71,6 +73,7 @@ type copier struct { pathCache pathCache download sourceDownloader tmpPaths []string + platform string } func copierFromDispatchRequest(req dispatchRequest, download sourceDownloader, imageSource *imageMount) copier { @@ -79,6 +82,7 @@ func copierFromDispatchRequest(req dispatchRequest, download sourceDownloader, i pathCache: req.builder.pathCache, download: download, imageSource: imageSource, + platform: req.builder.platform, } } @@ -86,13 +90,14 @@ func (o *copier) createCopyInstruction(args []string, cmdName string) (copyInstr inst := copyInstruction{cmdName: cmdName} last := len(args) - 1 - // Work in daemon-specific filepath semantics - inst.dest = filepath.FromSlash(args[last]) + // Work in platform-specific filepath semantics + inst.dest = fromSlash(args[last], o.platform) + separator := string(separator(o.platform)) infos, err := o.getCopyInfosForSourcePaths(args[0:last], inst.dest) if err != nil { return inst, errors.Wrapf(err, "%s failed", cmdName) } - if len(infos) > 1 && !strings.HasSuffix(inst.dest, string(os.PathSeparator)) { + if len(infos) > 1 && !strings.HasSuffix(inst.dest, separator) { return inst, errors.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName) } inst.infos = infos @@ -122,6 +127,7 @@ func (o *copier) getCopyInfoForSourcePath(orig, dest string) ([]copyInfo, error) if !urlutil.IsURL(orig) { return o.calcCopyInfo(orig, true) } + remote, path, err := o.download(orig) if err != nil { return nil, err @@ -134,7 +140,7 @@ func (o *copier) getCopyInfoForSourcePath(orig, dest string) ([]copyInfo, error) } path = unnamedFilename } - o.tmpPaths = append(o.tmpPaths, remote.Root()) + o.tmpPaths = append(o.tmpPaths, remote.Root().Path()) hash, err := remote.Hash(path) ci := newCopyInfoFromSource(remote, path, hash) @@ -154,14 +160,6 @@ func (o *copier) Cleanup() { // TODO: allowWildcards can probably be removed by refactoring this function further. func (o *copier) calcCopyInfo(origPath string, allowWildcards bool) ([]copyInfo, error) { imageSource := o.imageSource - if err := validateCopySourcePath(imageSource, origPath); err != nil { - return nil, err - } - - // Work in daemon-specific OS filepath semantics - origPath = filepath.FromSlash(origPath) - origPath = strings.TrimPrefix(origPath, string(os.PathSeparator)) - origPath = strings.TrimPrefix(origPath, "."+string(os.PathSeparator)) // TODO: do this when creating copier. Requires validateCopySourcePath // (and other below) to be aware of the difference sources. Why is it only @@ -178,8 +176,20 @@ func (o *copier) calcCopyInfo(origPath string, allowWildcards bool) ([]copyInfo, return nil, errors.Errorf("missing build context") } + root := o.source.Root() + + if err := validateCopySourcePath(imageSource, origPath, root.OS()); err != nil { + return nil, err + } + + // Work in source OS specific filepath semantics + // For LCOW, this is NOT the daemon OS. + origPath = root.FromSlash(origPath) + origPath = strings.TrimPrefix(origPath, string(root.Separator())) + origPath = strings.TrimPrefix(origPath, "."+string(root.Separator())) + // Deal with wildcards - if allowWildcards && containsWildcards(origPath) { + if allowWildcards && containsWildcards(origPath, root.OS()) { return o.copyWithWildcards(origPath) } @@ -211,6 +221,19 @@ func (o *copier) calcCopyInfo(origPath string, allowWildcards bool) ([]copyInfo, return newCopyInfos(newCopyInfoFromSource(o.source, origPath, hash)), nil } +func containsWildcards(name, platform string) bool { + isWindows := platform == "windows" + for i := 0; i < len(name); i++ { + ch := name[i] + if ch == '\\' && !isWindows { + i++ + } else if ch == '*' || ch == '?' || ch == '[' { + return true + } + } + return false +} + func (o *copier) storeInPathCache(im *imageMount, path string, hash string) { if im != nil { o.pathCache.Store(im.ImageID()+path, hash) @@ -218,12 +241,13 @@ func (o *copier) storeInPathCache(im *imageMount, path string, hash string) { } func (o *copier) copyWithWildcards(origPath string) ([]copyInfo, error) { + root := o.source.Root() var copyInfos []copyInfo - if err := filepath.Walk(o.source.Root(), func(path string, info os.FileInfo, err error) error { + if err := root.Walk(root.Path(), func(path string, info os.FileInfo, err error) error { if err != nil { return err } - rel, err := remotecontext.Rel(o.source.Root(), path) + rel, err := remotecontext.Rel(root, path) if err != nil { return err } @@ -231,7 +255,7 @@ func (o *copier) copyWithWildcards(origPath string) ([]copyInfo, error) { if rel == "." { return nil } - if match, _ := filepath.Match(origPath, rel); !match { + if match, _ := root.Match(origPath, rel); !match { return nil } @@ -273,7 +297,7 @@ func walkSource(source builder.Source, origPath string) ([]string, error) { } // Must be a dir var subfiles []string - err = filepath.Walk(fp, func(path string, info os.FileInfo, err error) error { + err = source.Root().Walk(fp, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -398,14 +422,19 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b return } - lc, err := remotecontext.NewLazySource(tmpDir) + lc, err := remotecontext.NewLazySource(containerfs.NewLocalContainerFS(tmpDir)) return lc, filename, err } type copyFileOptions struct { decompress bool - archiver *archive.Archiver chownPair idtools.IDPair + archiver Archiver +} + +type copyEndpoint struct { + driver containerfs.Driver + path string } func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions) error { @@ -413,6 +442,7 @@ func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions) if err != nil { return err } + destPath, err := dest.fullPath() if err != nil { return err @@ -420,59 +450,90 @@ func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions) archiver := options.archiver - src, err := os.Stat(srcPath) + srcEndpoint := ©Endpoint{driver: source.root, path: srcPath} + destEndpoint := ©Endpoint{driver: dest.root, path: destPath} + + src, err := source.root.Stat(srcPath) if err != nil { return errors.Wrapf(err, "source path not found") } if src.IsDir() { - return copyDirectory(archiver, srcPath, destPath, options.chownPair) + return copyDirectory(archiver, srcEndpoint, destEndpoint, options.chownPair) } - if options.decompress && archive.IsArchivePath(srcPath) && !source.noDecompress { + if options.decompress && isArchivePath(source.root, srcPath) && !source.noDecompress { return archiver.UntarPath(srcPath, destPath) } - destExistsAsDir, err := isExistingDirectory(destPath) + destExistsAsDir, err := isExistingDirectory(destEndpoint) if err != nil { return err } // dest.path must be used because destPath has already been cleaned of any // trailing slash - if endsInSlash(dest.path) || destExistsAsDir { + if endsInSlash(dest.root, dest.path) || destExistsAsDir { // source.path must be used to get the correct filename when the source // is a symlink - destPath = filepath.Join(destPath, filepath.Base(source.path)) + destPath = dest.root.Join(destPath, source.root.Base(source.path)) + destEndpoint = ©Endpoint{driver: dest.root, path: destPath} } - return copyFile(archiver, srcPath, destPath, options.chownPair) + return copyFile(archiver, srcEndpoint, destEndpoint, options.chownPair) } -func copyDirectory(archiver *archive.Archiver, source, dest string, chownPair idtools.IDPair) error { +func isArchivePath(driver containerfs.ContainerFS, path string) bool { + file, err := driver.Open(path) + if err != nil { + return false + } + defer file.Close() + rdr, err := archive.DecompressStream(file) + if err != nil { + return false + } + r := tar.NewReader(rdr) + _, err = r.Next() + return err == nil +} + +func copyDirectory(archiver Archiver, source, dest *copyEndpoint, chownPair idtools.IDPair) error { destExists, err := isExistingDirectory(dest) if err != nil { return errors.Wrapf(err, "failed to query destination path") } - if err := archiver.CopyWithTar(source, dest); err != nil { + + if err := archiver.CopyWithTar(source.path, dest.path); err != nil { return errors.Wrapf(err, "failed to copy directory") } - return fixPermissions(source, dest, chownPair, !destExists) + // TODO: @gupta-ak. Investigate how LCOW permission mappings will work. + return fixPermissions(source.path, dest.path, chownPair, !destExists) } -func copyFile(archiver *archive.Archiver, source, dest string, chownPair idtools.IDPair) error { - if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest), 0755, chownPair); err != nil { - return errors.Wrapf(err, "failed to create new directory") +func copyFile(archiver Archiver, source, dest *copyEndpoint, chownPair idtools.IDPair) error { + if runtime.GOOS == "windows" && dest.driver.OS() == "linux" { + // LCOW + if err := dest.driver.MkdirAll(dest.driver.Dir(dest.path), 0755); err != nil { + return errors.Wrapf(err, "failed to create new directory") + } + } else { + if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest.path), 0755, chownPair); err != nil { + // Normal containers + return errors.Wrapf(err, "failed to create new directory") + } } - if err := archiver.CopyFileWithTar(source, dest); err != nil { + + if err := archiver.CopyFileWithTar(source.path, dest.path); err != nil { return errors.Wrapf(err, "failed to copy file") } - return fixPermissions(source, dest, chownPair, false) + // TODO: @gupta-ak. Investigate how LCOW permission mappings will work. + return fixPermissions(source.path, dest.path, chownPair, false) } -func endsInSlash(path string) bool { - return strings.HasSuffix(path, string(os.PathSeparator)) +func endsInSlash(driver containerfs.Driver, path string) bool { + return strings.HasSuffix(path, string(driver.Separator())) } // isExistingDirectory returns true if the path exists and is a directory -func isExistingDirectory(path string) (bool, error) { - destStat, err := os.Stat(path) +func isExistingDirectory(point *copyEndpoint) (bool, error) { + destStat, err := point.driver.Stat(point.path) switch { case os.IsNotExist(err): return false, nil diff --git a/builder/dockerfile/copy_test.go b/builder/dockerfile/copy_test.go index 86f4dcff30..87c5675d90 100644 --- a/builder/dockerfile/copy_test.go +++ b/builder/dockerfile/copy_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "github.com/docker/docker/pkg/containerfs" "github.com/gotestyourself/gotestyourself/fs" "github.com/stretchr/testify/assert" ) @@ -37,7 +38,7 @@ func TestIsExistingDirectory(t *testing.T) { } for _, testcase := range testcases { - result, err := isExistingDirectory(testcase.path) + result, err := isExistingDirectory(©Endpoint{driver: containerfs.NewLocalDriver(), path: testcase.path}) if !assert.NoError(t, err) { continue } diff --git a/builder/dockerfile/copy_unix.go b/builder/dockerfile/copy_unix.go index a4a5e05235..8833700554 100644 --- a/builder/dockerfile/copy_unix.go +++ b/builder/dockerfile/copy_unix.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" ) @@ -15,7 +16,8 @@ func fixPermissions(source, destination string, rootIDs idtools.IDPair, override err error ) if !overrideSkip { - skipChownRoot, err = isExistingDirectory(destination) + destEndpoint := ©Endpoint{driver: containerfs.NewLocalDriver(), path: destination} + skipChownRoot, err = isExistingDirectory(destEndpoint) if err != nil { return err } @@ -40,3 +42,7 @@ func fixPermissions(source, destination string, rootIDs idtools.IDPair, override return os.Lchown(fullpath, rootIDs.UID, rootIDs.GID) }) } + +func validateCopySourcePath(imageSource *imageMount, origPath, platform string) error { + return nil +} diff --git a/builder/dockerfile/copy_windows.go b/builder/dockerfile/copy_windows.go index e4b15bcc10..dcf4c5acfb 100644 --- a/builder/dockerfile/copy_windows.go +++ b/builder/dockerfile/copy_windows.go @@ -1,8 +1,38 @@ package dockerfile -import "github.com/docker/docker/pkg/idtools" +import ( + "errors" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/idtools" +) func fixPermissions(source, destination string, rootIDs idtools.IDPair, overrideSkip bool) error { // chown is not supported on Windows return nil } + +func validateCopySourcePath(imageSource *imageMount, origPath, platform string) error { + // validate windows paths from other images + LCOW + if imageSource == nil || platform != "windows" { + return nil + } + + origPath = filepath.FromSlash(origPath) + p := strings.ToLower(filepath.Clean(origPath)) + if !filepath.IsAbs(p) { + if filepath.VolumeName(p) != "" { + if p[len(p)-2:] == ":." { // case where clean returns weird c:. paths + p = p[:len(p)-1] + } + p += "\\" + } else { + p = filepath.Join("c:\\", p) + } + } + if _, blacklisted := pathBlacklist[p]; blacklisted { + return errors.New("copy from c:\\ or c:\\windows is not allowed on windows") + } + return nil +} diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index 5424e75841..04ed6dc337 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -7,6 +7,9 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "io" + "os" + "path" "path/filepath" "strconv" "strings" @@ -15,13 +18,69 @@ import ( "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" "github.com/docker/docker/image" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/system" lcUser "github.com/opencontainers/runc/libcontainer/user" "github.com/pkg/errors" ) +// For Windows only +var pathBlacklist = map[string]bool{ + "c:\\": true, + "c:\\windows": true, +} + +// Archiver defines an interface for copying files from one destination to +// another using Tar/Untar. +type Archiver interface { + TarUntar(src, dst string) error + UntarPath(src, dst string) error + CopyWithTar(src, dst string) error + CopyFileWithTar(src, dst string) error + IDMappings() *idtools.IDMappings +} + +// The builder will use the following interfaces if the container fs implements +// these for optimized copies to and from the container. +type extractor interface { + ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error +} + +type archiver interface { + ArchivePath(src string, opts *archive.TarOptions) (io.ReadCloser, error) +} + +// helper functions to get tar/untar func +func untarFunc(i interface{}) containerfs.UntarFunc { + if ea, ok := i.(extractor); ok { + return ea.ExtractArchive + } + return chrootarchive.Untar +} + +func tarFunc(i interface{}) containerfs.TarFunc { + if ap, ok := i.(archiver); ok { + return ap.ArchivePath + } + return archive.TarWithOptions +} + +func (b *Builder) getArchiver(src, dst containerfs.Driver) Archiver { + t, u := tarFunc(src), untarFunc(dst) + return &containerfs.Archiver{ + SrcDriver: src, + DstDriver: dst, + Tar: t, + Untar: u, + IDMappingsVar: b.idMappings, + } +} + func (b *Builder) commit(dispatchState *dispatchState, comment string) error { if b.disableCommit { return nil @@ -131,28 +190,29 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error if err != nil { return errors.Wrapf(err, "failed to get destination image %q", state.imageID) } - destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, imageMount) + + destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, imageMount, b.platform) if err != nil { return err } - chownPair := b.archiver.IDMappings.RootPair() + chownPair := b.idMappings.RootPair() // if a chown was requested, perform the steps to get the uid, gid // translated (if necessary because of user namespaces), and replace // the root pair with the chown pair for copy operations if inst.chownStr != "" { - chownPair, err = parseChownFlag(inst.chownStr, destInfo.root, b.archiver.IDMappings) + chownPair, err = parseChownFlag(inst.chownStr, destInfo.root.Path(), b.idMappings) if err != nil { return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping") } } - opts := copyFileOptions{ - decompress: inst.allowLocalDecompression, - archiver: b.archiver, - chownPair: chownPair, - } for _, info := range inst.infos { + opts := copyFileOptions{ + decompress: inst.allowLocalDecompression, + archiver: b.getArchiver(info.root, destInfo.root), + chownPair: chownPair, + } if err := performCopyForInfo(destInfo, info, opts); err != nil { return errors.Wrapf(err, "failed to copy files") } @@ -236,10 +296,10 @@ func lookupGroup(groupStr, filepath string) (int, error) { return groups[0].Gid, nil } -func createDestInfo(workingDir string, inst copyInstruction, imageMount *imageMount) (copyInfo, error) { +func createDestInfo(workingDir string, inst copyInstruction, imageMount *imageMount, platform string) (copyInfo, error) { // Twiddle the destination when it's a relative path - meaning, make it // relative to the WORKINGDIR - dest, err := normalizeDest(workingDir, inst.dest) + dest, err := normalizeDest(workingDir, inst.dest, platform) if err != nil { return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName) } @@ -252,6 +312,63 @@ func createDestInfo(workingDir string, inst copyInstruction, imageMount *imageMo return newCopyInfoFromSource(destMount, dest, ""), nil } +// normalizeDest normalises the destination of a COPY/ADD command in a +// platform semantically consistent way. +func normalizeDest(workingDir, requested string, platform string) (string, error) { + dest := fromSlash(requested, platform) + endsInSlash := strings.HasSuffix(dest, string(separator(platform))) + + if platform != "windows" { + if !path.IsAbs(requested) { + dest = path.Join("/", filepath.ToSlash(workingDir), dest) + // Make sure we preserve any trailing slash + if endsInSlash { + dest += "/" + } + } + return dest, nil + } + + // We are guaranteed that the working directory is already consistent, + // However, Windows also has, for now, the limitation that ADD/COPY can + // only be done to the system drive, not any drives that might be present + // as a result of a bind mount. + // + // So... if the path requested is Linux-style absolute (/foo or \\foo), + // we assume it is the system drive. If it is a Windows-style absolute + // (DRIVE:\\foo), error if DRIVE is not C. And finally, ensure we + // strip any configured working directories drive letter so that it + // can be subsequently legitimately converted to a Windows volume-style + // pathname. + + // Not a typo - filepath.IsAbs, not system.IsAbs on this next check as + // we only want to validate where the DriveColon part has been supplied. + if filepath.IsAbs(dest) { + if strings.ToUpper(string(dest[0])) != "C" { + return "", fmt.Errorf("Windows does not support destinations not on the system drive (C:)") + } + dest = dest[2:] // Strip the drive letter + } + + // Cannot handle relative where WorkingDir is not the system drive. + if len(workingDir) > 0 { + if ((len(workingDir) > 1) && !system.IsAbs(workingDir[2:])) || (len(workingDir) == 1) { + return "", fmt.Errorf("Current WorkingDir %s is not platform consistent", workingDir) + } + if !system.IsAbs(dest) { + if string(workingDir[0]) != "C" { + return "", fmt.Errorf("Windows does not support relative paths when WORKDIR is not the system drive") + } + dest = filepath.Join(string(os.PathSeparator), workingDir[2:], dest) + // Make sure we preserve any trailing slash + if endsInSlash { + dest += string(os.PathSeparator) + } + } + } + return dest, nil +} + // For backwards compat, if there's just one info then use it as the // cache look-up string, otherwise hash 'em all into one func getSourceHashFromInfos(infos []copyInfo) string { @@ -397,3 +514,19 @@ func hostConfigFromOptions(options *types.ImageBuildOptions) *container.HostConf ExtraHosts: options.ExtraHosts, } } + +// fromSlash works like filepath.FromSlash but with a given OS platform field +func fromSlash(path, platform string) string { + if platform == "windows" { + return strings.Replace(path, "/", "\\", -1) + } + return path +} + +// separator returns a OS path separator for the given OS platform +func separator(platform string) byte { + if platform == "windows" { + return '\\' + } + return '/' +} diff --git a/builder/dockerfile/internals_unix.go b/builder/dockerfile/internals_unix.go deleted file mode 100644 index 533735c960..0000000000 --- a/builder/dockerfile/internals_unix.go +++ /dev/null @@ -1,42 +0,0 @@ -// +build !windows - -package dockerfile - -import ( - "os" - "path/filepath" - "strings" - - "github.com/docker/docker/pkg/system" -) - -// normalizeDest normalizes the destination of a COPY/ADD command in a -// platform semantically consistent way. -func normalizeDest(workingDir, requested string) (string, error) { - dest := filepath.FromSlash(requested) - endsInSlash := strings.HasSuffix(requested, string(os.PathSeparator)) - if !system.IsAbs(requested) { - dest = filepath.Join(string(os.PathSeparator), filepath.FromSlash(workingDir), dest) - // Make sure we preserve any trailing slash - if endsInSlash { - dest += string(os.PathSeparator) - } - } - return dest, nil -} - -func containsWildcards(name string) bool { - for i := 0; i < len(name); i++ { - ch := name[i] - if ch == '\\' { - i++ - } else if ch == '*' || ch == '?' || ch == '[' { - return true - } - } - return false -} - -func validateCopySourcePath(imageSource *imageMount, origPath string) error { - return nil -} diff --git a/builder/dockerfile/internals_windows.go b/builder/dockerfile/internals_windows.go deleted file mode 100644 index 57f83296ab..0000000000 --- a/builder/dockerfile/internals_windows.go +++ /dev/null @@ -1,95 +0,0 @@ -package dockerfile - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/docker/docker/pkg/system" - "github.com/pkg/errors" -) - -// normalizeDest normalizes the destination of a COPY/ADD command in a -// platform semantically consistent way. -func normalizeDest(workingDir, requested string) (string, error) { - dest := filepath.FromSlash(requested) - endsInSlash := strings.HasSuffix(dest, string(os.PathSeparator)) - - // We are guaranteed that the working directory is already consistent, - // However, Windows also has, for now, the limitation that ADD/COPY can - // only be done to the system drive, not any drives that might be present - // as a result of a bind mount. - // - // So... if the path requested is Linux-style absolute (/foo or \\foo), - // we assume it is the system drive. If it is a Windows-style absolute - // (DRIVE:\\foo), error if DRIVE is not C. And finally, ensure we - // strip any configured working directories drive letter so that it - // can be subsequently legitimately converted to a Windows volume-style - // pathname. - - // Not a typo - filepath.IsAbs, not system.IsAbs on this next check as - // we only want to validate where the DriveColon part has been supplied. - if filepath.IsAbs(dest) { - if strings.ToUpper(string(dest[0])) != "C" { - return "", fmt.Errorf("Windows does not support destinations not on the system drive (C:)") - } - dest = dest[2:] // Strip the drive letter - } - - // Cannot handle relative where WorkingDir is not the system drive. - if len(workingDir) > 0 { - if ((len(workingDir) > 1) && !system.IsAbs(workingDir[2:])) || (len(workingDir) == 1) { - return "", fmt.Errorf("Current WorkingDir %s is not platform consistent", workingDir) - } - if !system.IsAbs(dest) { - if string(workingDir[0]) != "C" { - return "", fmt.Errorf("Windows does not support relative paths when WORKDIR is not the system drive") - } - dest = filepath.Join(string(os.PathSeparator), workingDir[2:], dest) - // Make sure we preserve any trailing slash - if endsInSlash { - dest += string(os.PathSeparator) - } - } - } - return dest, nil -} - -func containsWildcards(name string) bool { - for i := 0; i < len(name); i++ { - ch := name[i] - if ch == '*' || ch == '?' || ch == '[' { - return true - } - } - return false -} - -var pathBlacklist = map[string]bool{ - "c:\\": true, - "c:\\windows": true, -} - -func validateCopySourcePath(imageSource *imageMount, origPath string) error { - // validate windows paths from other images - if imageSource == nil { - return nil - } - origPath = filepath.FromSlash(origPath) - p := strings.ToLower(filepath.Clean(origPath)) - if !filepath.IsAbs(p) { - if filepath.VolumeName(p) != "" { - if p[len(p)-2:] == ":." { // case where clean returns weird c:. paths - p = p[:len(p)-1] - } - p += "\\" - } else { - p = filepath.Join("c:\\", p) - } - } - if _, blacklisted := pathBlacklist[p]; blacklisted { - return errors.New("copy from c:\\ or c:\\windows is not allowed on windows") - } - return nil -} diff --git a/builder/dockerfile/internals_windows_test.go b/builder/dockerfile/internals_windows_test.go index ca6920c3de..6ecc37ba63 100644 --- a/builder/dockerfile/internals_windows_test.go +++ b/builder/dockerfile/internals_windows_test.go @@ -40,7 +40,7 @@ func TestNormalizeDest(t *testing.T) { } for _, testcase := range tests { msg := fmt.Sprintf("Input: %s, %s", testcase.current, testcase.requested) - actual, err := normalizeDest(testcase.current, testcase.requested) + actual, err := normalizeDest(testcase.current, testcase.requested, "windows") if testcase.etext == "" { if !assert.NoError(t, err, msg) { continue diff --git a/builder/dockerfile/mockbackend_test.go b/builder/dockerfile/mockbackend_test.go index adc22762e0..0f076b5326 100644 --- a/builder/dockerfile/mockbackend_test.go +++ b/builder/dockerfile/mockbackend_test.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/builder" containerpkg "github.com/docker/docker/container" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/containerfs" "golang.org/x/net/context" ) @@ -117,8 +118,8 @@ func (l *mockLayer) Release() error { return nil } -func (l *mockLayer) Mount() (string, error) { - return "mountPath", nil +func (l *mockLayer) Mount() (containerfs.ContainerFS, error) { + return containerfs.NewLocalContainerFS("mountPath"), nil } func (l *mockLayer) Commit(string) (builder.ReleaseableLayer, error) { diff --git a/builder/fscache/fscache_test.go b/builder/fscache/fscache_test.go index 3f6a1b02af..c327ec72d3 100644 --- a/builder/fscache/fscache_test.go +++ b/builder/fscache/fscache_test.go @@ -36,25 +36,25 @@ func TestFSCache(t *testing.T) { src1, err := fscache.SyncFrom(context.TODO(), &testIdentifier{"foo", "data", "bar"}) assert.Nil(t, err) - dt, err := ioutil.ReadFile(filepath.Join(src1.Root(), "foo")) + dt, err := ioutil.ReadFile(filepath.Join(src1.Root().Path(), "foo")) assert.Nil(t, err) assert.Equal(t, string(dt), "data") // same id doesn't recalculate anything src2, err := fscache.SyncFrom(context.TODO(), &testIdentifier{"foo", "data2", "bar"}) assert.Nil(t, err) - assert.Equal(t, src1.Root(), src2.Root()) + assert.Equal(t, src1.Root().Path(), src2.Root().Path()) - dt, err = ioutil.ReadFile(filepath.Join(src1.Root(), "foo")) + dt, err = ioutil.ReadFile(filepath.Join(src1.Root().Path(), "foo")) assert.Nil(t, err) assert.Equal(t, string(dt), "data") assert.Nil(t, src2.Close()) src3, err := fscache.SyncFrom(context.TODO(), &testIdentifier{"foo2", "data2", "bar"}) assert.Nil(t, err) - assert.NotEqual(t, src1.Root(), src3.Root()) + assert.NotEqual(t, src1.Root().Path(), src3.Root().Path()) - dt, err = ioutil.ReadFile(filepath.Join(src3.Root(), "foo2")) + dt, err = ioutil.ReadFile(filepath.Join(src3.Root().Path(), "foo2")) assert.Nil(t, err) assert.Equal(t, string(dt), "data2") @@ -71,12 +71,12 @@ func TestFSCache(t *testing.T) { // new upload with the same shared key shoutl overwrite src4, err := fscache.SyncFrom(context.TODO(), &testIdentifier{"foo3", "data3", "bar"}) assert.Nil(t, err) - assert.NotEqual(t, src1.Root(), src3.Root()) + assert.NotEqual(t, src1.Root().Path(), src3.Root().Path()) - dt, err = ioutil.ReadFile(filepath.Join(src3.Root(), "foo3")) + dt, err = ioutil.ReadFile(filepath.Join(src3.Root().Path(), "foo3")) assert.Nil(t, err) assert.Equal(t, string(dt), "data3") - assert.Equal(t, src4.Root(), src3.Root()) + assert.Equal(t, src4.Root().Path(), src3.Root().Path()) assert.Nil(t, src4.Close()) s, err = fscache.DiskUsage() diff --git a/builder/remotecontext/archive.go b/builder/remotecontext/archive.go index f48cafecd4..fc18c5da31 100644 --- a/builder/remotecontext/archive.go +++ b/builder/remotecontext/archive.go @@ -8,19 +8,19 @@ import ( "github.com/docker/docker/builder" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/tarsum" "github.com/pkg/errors" ) type archiveContext struct { - root string + root containerfs.ContainerFS sums tarsum.FileInfoSums } func (c *archiveContext) Close() error { - return os.RemoveAll(c.root) + return c.root.RemoveAll(c.root.Path()) } func convertPathError(err error, cleanpath string) error { @@ -52,7 +52,8 @@ func FromArchive(tarStream io.Reader) (builder.Source, error) { return nil, err } - tsc := &archiveContext{root: root} + // Assume local file system. Since it's coming from a tar file. + tsc := &archiveContext{root: containerfs.NewLocalContainerFS(root)} // Make sure we clean-up upon error. In the happy case the caller // is expected to manage the clean-up @@ -82,7 +83,7 @@ func FromArchive(tarStream io.Reader) (builder.Source, error) { return tsc, nil } -func (c *archiveContext) Root() string { +func (c *archiveContext) Root() containerfs.ContainerFS { return c.root } @@ -91,7 +92,7 @@ func (c *archiveContext) Remove(path string) error { if err != nil { return err } - return os.RemoveAll(fullpath) + return c.root.RemoveAll(fullpath) } func (c *archiveContext) Hash(path string) (string, error) { @@ -100,7 +101,7 @@ func (c *archiveContext) Hash(path string) (string, error) { return "", err } - rel, err := filepath.Rel(c.root, fullpath) + rel, err := c.root.Rel(c.root.Path(), fullpath) if err != nil { return "", convertPathError(err, cleanpath) } @@ -115,13 +116,13 @@ func (c *archiveContext) Hash(path string) (string, error) { return path, nil // backwards compat TODO: see if really needed } -func normalize(path, root string) (cleanPath, fullPath string, err error) { - cleanPath = filepath.Clean(string(os.PathSeparator) + path)[1:] - fullPath, err = symlink.FollowSymlinkInScope(filepath.Join(root, path), root) +func normalize(path string, root containerfs.ContainerFS) (cleanPath, fullPath string, err error) { + cleanPath = root.Clean(string(root.Separator()) + path)[1:] + fullPath, err = root.ResolveScopedPath(path, true) if err != nil { return "", "", errors.Wrapf(err, "forbidden path outside the build context: %s (%s)", path, cleanPath) } - if _, err := os.Lstat(fullPath); err != nil { + if _, err := root.Lstat(fullPath); err != nil { return "", "", errors.WithStack(convertPathError(err, path)) } return diff --git a/builder/remotecontext/detect.go b/builder/remotecontext/detect.go index ec32dbed7a..38aff67985 100644 --- a/builder/remotecontext/detect.go +++ b/builder/remotecontext/detect.go @@ -5,15 +5,14 @@ import ( "fmt" "io" "os" - "path/filepath" "strings" + "github.com/containerd/continuity/driver" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/builder" "github.com/docker/docker/builder/dockerfile/parser" "github.com/docker/docker/builder/dockerignore" "github.com/docker/docker/pkg/fileutils" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/urlutil" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -157,12 +156,12 @@ func readAndParseDockerfile(name string, rc io.Reader) (*parser.Result, error) { return parser.Parse(br) } -func openAt(remote builder.Source, path string) (*os.File, error) { +func openAt(remote builder.Source, path string) (driver.File, error) { fullPath, err := FullPath(remote, path) if err != nil { return nil, err } - return os.Open(fullPath) + return remote.Root().Open(fullPath) } // StatAt is a helper for calling Stat on a path from a source @@ -171,12 +170,12 @@ func StatAt(remote builder.Source, path string) (os.FileInfo, error) { if err != nil { return nil, err } - return os.Stat(fullPath) + return remote.Root().Stat(fullPath) } // FullPath is a helper for getting a full path for a path from a source func FullPath(remote builder.Source, path string) (string, error) { - fullPath, err := symlink.FollowSymlinkInScope(filepath.Join(remote.Root(), path), remote.Root()) + fullPath, err := remote.Root().ResolveScopedPath(path, true) if err != nil { return "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullPath) // backwards compat with old error } diff --git a/builder/remotecontext/detect_test.go b/builder/remotecontext/detect_test.go index d5dfa495bb..3d1ebd1c3f 100644 --- a/builder/remotecontext/detect_test.go +++ b/builder/remotecontext/detect_test.go @@ -5,11 +5,11 @@ import ( "io/ioutil" "log" "os" - "path/filepath" "sort" "testing" "github.com/docker/docker/builder" + "github.com/docker/docker/pkg/containerfs" ) const ( @@ -53,7 +53,7 @@ func checkDirectory(t *testing.T, dir string, expectedFiles []string) { } func executeProcess(t *testing.T, contextDir string) { - modifiableCtx := &stubRemote{root: contextDir} + modifiableCtx := &stubRemote{root: containerfs.NewLocalContainerFS(contextDir)} err := removeDockerfile(modifiableCtx, builder.DefaultDockerfileName) @@ -105,19 +105,19 @@ func TestProcessShouldLeaveAllFiles(t *testing.T) { // TODO: remove after moving to a separate pkg type stubRemote struct { - root string + root containerfs.ContainerFS } func (r *stubRemote) Hash(path string) (string, error) { return "", errors.New("not implemented") } -func (r *stubRemote) Root() string { +func (r *stubRemote) Root() containerfs.ContainerFS { return r.root } func (r *stubRemote) Close() error { return errors.New("not implemented") } func (r *stubRemote) Remove(p string) error { - return os.Remove(filepath.Join(r.root, p)) + return r.root.Remove(r.root.Join(r.root.Path(), p)) } diff --git a/builder/remotecontext/lazycontext.go b/builder/remotecontext/lazycontext.go index b29c413fac..66f36defd4 100644 --- a/builder/remotecontext/lazycontext.go +++ b/builder/remotecontext/lazycontext.go @@ -3,11 +3,10 @@ package remotecontext import ( "encoding/hex" "os" - "path/filepath" - "runtime" "strings" "github.com/docker/docker/builder" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/pools" "github.com/pkg/errors" ) @@ -15,7 +14,7 @@ import ( // NewLazySource creates a new LazyContext. LazyContext defines a hashed build // context based on a root directory. Individual files are hashed first time // they are asked. It is not safe to call methods of LazyContext concurrently. -func NewLazySource(root string) (builder.Source, error) { +func NewLazySource(root containerfs.ContainerFS) (builder.Source, error) { return &lazySource{ root: root, sums: make(map[string]string), @@ -23,11 +22,11 @@ func NewLazySource(root string) (builder.Source, error) { } type lazySource struct { - root string + root containerfs.ContainerFS sums map[string]string } -func (c *lazySource) Root() string { +func (c *lazySource) Root() containerfs.ContainerFS { return c.root } @@ -41,7 +40,7 @@ func (c *lazySource) Hash(path string) (string, error) { return "", err } - fi, err := os.Lstat(fullPath) + fi, err := c.root.Lstat(fullPath) if err != nil { return "", errors.WithStack(err) } @@ -63,13 +62,13 @@ func (c *lazySource) Hash(path string) (string, error) { } func (c *lazySource) prepareHash(relPath string, fi os.FileInfo) (string, error) { - p := filepath.Join(c.root, relPath) + p := c.root.Join(c.root.Path(), relPath) h, err := NewFileHash(p, relPath, fi) if err != nil { return "", errors.Wrapf(err, "failed to create hash for %s", relPath) } if fi.Mode().IsRegular() && fi.Size() > 0 { - f, err := os.Open(p) + f, err := c.root.Open(p) if err != nil { return "", errors.Wrapf(err, "failed to open %s", relPath) } @@ -85,10 +84,10 @@ func (c *lazySource) prepareHash(relPath string, fi os.FileInfo) (string, error) // Rel makes a path relative to base path. Same as `filepath.Rel` but can also // handle UUID paths in windows. -func Rel(basepath, targpath string) (string, error) { +func Rel(basepath containerfs.ContainerFS, targpath string) (string, error) { // filepath.Rel can't handle UUID paths in windows - if runtime.GOOS == "windows" { - pfx := basepath + `\` + if basepath.OS() == "windows" { + pfx := basepath.Path() + `\` if strings.HasPrefix(targpath, pfx) { p := strings.TrimPrefix(targpath, pfx) if p == "" { @@ -97,5 +96,5 @@ func Rel(basepath, targpath string) (string, error) { return p, nil } } - return filepath.Rel(basepath, targpath) + return basepath.Rel(basepath.Path(), targpath) } diff --git a/builder/remotecontext/tarsum.go b/builder/remotecontext/tarsum.go index fec5a6184d..6770eed871 100644 --- a/builder/remotecontext/tarsum.go +++ b/builder/remotecontext/tarsum.go @@ -3,11 +3,11 @@ package remotecontext import ( "fmt" "os" - "path/filepath" "sync" - "github.com/docker/docker/pkg/symlink" iradix "github.com/hashicorp/go-immutable-radix" + + "github.com/docker/docker/pkg/containerfs" "github.com/pkg/errors" "github.com/tonistiigi/fsutil" ) @@ -19,7 +19,7 @@ type hashed interface { // CachableSource is a source that contains cache records for its contents type CachableSource struct { mu sync.Mutex - root string + root containerfs.ContainerFS tree *iradix.Tree txn *iradix.Txn } @@ -28,7 +28,7 @@ type CachableSource struct { func NewCachableSource(root string) *CachableSource { ts := &CachableSource{ tree: iradix.New(), - root: root, + root: containerfs.NewLocalContainerFS(root), } return ts } @@ -67,7 +67,7 @@ func (cs *CachableSource) Scan() error { return err } txn := iradix.New().Txn() - err = filepath.Walk(cs.root, func(path string, info os.FileInfo, err error) error { + err = cs.root.Walk(cs.root.Path(), func(path string, info os.FileInfo, err error) error { if err != nil { return errors.Wrapf(err, "failed to walk %s", path) } @@ -134,12 +134,12 @@ func (cs *CachableSource) Close() error { } func (cs *CachableSource) normalize(path string) (cleanpath, fullpath string, err error) { - cleanpath = filepath.Clean(string(os.PathSeparator) + path)[1:] - fullpath, err = symlink.FollowSymlinkInScope(filepath.Join(cs.root, path), cs.root) + cleanpath = cs.root.Clean(string(cs.root.Separator()) + path)[1:] + fullpath, err = cs.root.ResolveScopedPath(path, true) if err != nil { return "", "", fmt.Errorf("Forbidden path outside the context: %s (%s)", path, fullpath) } - _, err = os.Lstat(fullpath) + _, err = cs.root.Lstat(fullpath) if err != nil { return "", "", convertPathError(err, path) } @@ -158,7 +158,7 @@ func (cs *CachableSource) Hash(path string) (string, error) { } // Root returns a root directory for the source -func (cs *CachableSource) Root() string { +func (cs *CachableSource) Root() containerfs.ContainerFS { return cs.root } diff --git a/builder/remotecontext/tarsum.pb.go b/builder/remotecontext/tarsum.pb.go index 561a7f6367..1d23bbe65b 100644 --- a/builder/remotecontext/tarsum.pb.go +++ b/builder/remotecontext/tarsum.pb.go @@ -94,7 +94,7 @@ func (this *TarsumBackup) GoString() string { s := make([]string, 0, 5) s = append(s, "&remotecontext.TarsumBackup{") keysForHashes := make([]string, 0, len(this.Hashes)) - for k, _ := range this.Hashes { + for k := range this.Hashes { keysForHashes = append(keysForHashes, k) } github_com_gogo_protobuf_sortkeys.Strings(keysForHashes) @@ -133,7 +133,7 @@ func (m *TarsumBackup) MarshalTo(dAtA []byte) (int, error) { var l int _ = l if len(m.Hashes) > 0 { - for k, _ := range m.Hashes { + for k := range m.Hashes { dAtA[i] = 0xa i++ v := m.Hashes[k] @@ -211,7 +211,7 @@ func (this *TarsumBackup) String() string { return "nil" } keysForHashes := make([]string, 0, len(this.Hashes)) - for k, _ := range this.Hashes { + for k := range this.Hashes { keysForHashes = append(keysForHashes, k) } github_com_gogo_protobuf_sortkeys.Strings(keysForHashes) diff --git a/builder/remotecontext/tarsum_test.go b/builder/remotecontext/tarsum_test.go index 8a9d69bb73..6d2b41d3d4 100644 --- a/builder/remotecontext/tarsum_test.go +++ b/builder/remotecontext/tarsum_test.go @@ -35,7 +35,7 @@ func TestCloseRootDirectory(t *testing.T) { t.Fatalf("Error while executing Close: %s", err) } - _, err = os.Stat(src.Root()) + _, err = os.Stat(src.Root().Path()) if !os.IsNotExist(err) { t.Fatal("Directory should not exist at this point") diff --git a/container/archive.go b/container/archive.go index 56e6598b9c..ec4c236fe6 100644 --- a/container/archive.go +++ b/container/archive.go @@ -2,7 +2,6 @@ package container import ( "os" - "path/filepath" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/archive" @@ -15,17 +14,20 @@ import ( // an error if the path points to outside the container's rootfs. func (container *Container) ResolvePath(path string) (resolvedPath, absPath string, err error) { // Check if a drive letter supplied, it must be the system drive. No-op except on Windows - path, err = system.CheckSystemDriveAndRemoveDriveLetter(path) + path, err = system.CheckSystemDriveAndRemoveDriveLetter(path, container.BaseFS) if err != nil { return "", "", err } // Consider the given path as an absolute path in the container. - absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path) + absPath = archive.PreserveTrailingDotOrSeparator( + container.BaseFS.Join(string(container.BaseFS.Separator()), path), + path, + container.BaseFS.Separator()) // Split the absPath into its Directory and Base components. We will // resolve the dir in the scope of the container then append the base. - dirPath, basePath := filepath.Split(absPath) + dirPath, basePath := container.BaseFS.Split(absPath) resolvedDirPath, err := container.GetResourcePath(dirPath) if err != nil { @@ -34,8 +36,7 @@ func (container *Container) ResolvePath(path string) (resolvedPath, absPath stri // resolvedDirPath will have been cleaned (no trailing path separators) so // we can manually join it with the base path element. - resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath - + resolvedPath = resolvedDirPath + string(container.BaseFS.Separator()) + basePath return resolvedPath, absPath, nil } @@ -44,7 +45,9 @@ func (container *Container) ResolvePath(path string) (resolvedPath, absPath stri // resolved to a path on the host corresponding to the given absolute path // inside the container. func (container *Container) StatPath(resolvedPath, absPath string) (stat *types.ContainerPathStat, err error) { - lstat, err := os.Lstat(resolvedPath) + driver := container.BaseFS + + lstat, err := driver.Lstat(resolvedPath) if err != nil { return nil, err } @@ -57,17 +60,17 @@ func (container *Container) StatPath(resolvedPath, absPath string) (stat *types. return nil, err } - linkTarget, err = filepath.Rel(container.BaseFS, hostPath) + linkTarget, err = driver.Rel(driver.Path(), hostPath) if err != nil { return nil, err } // Make it an absolute path. - linkTarget = filepath.Join(string(filepath.Separator), linkTarget) + linkTarget = driver.Join(string(driver.Separator()), linkTarget) } return &types.ContainerPathStat{ - Name: filepath.Base(absPath), + Name: driver.Base(absPath), Size: lstat.Size(), Mode: lstat.Mode(), Mtime: lstat.ModTime(), diff --git a/container/container.go b/container/container.go index 188c017cf9..3791ed8e76 100644 --- a/container/container.go +++ b/container/container.go @@ -28,6 +28,7 @@ import ( "github.com/docker/docker/layer" "github.com/docker/docker/libcontainerd" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/signal" @@ -64,10 +65,10 @@ var ( type Container struct { StreamConfig *stream.Config // embed for Container to support states directly. - *State `json:"State"` // Needed for Engine API version <= 1.11 - Root string `json:"-"` // Path to the "home" of the container, including metadata. - BaseFS string `json:"-"` // Path to the graphdriver mountpoint - RWLayer layer.RWLayer `json:"-"` + *State `json:"State"` // Needed for Engine API version <= 1.11 + Root string `json:"-"` // Path to the "home" of the container, including metadata. + BaseFS containerfs.ContainerFS `json:"-"` // interface containing graphdriver mount + RWLayer layer.RWLayer `json:"-"` ID string Created time.Time Managed bool @@ -305,15 +306,13 @@ func (container *Container) SetupWorkingDirectory(rootIDs idtools.IDPair) error func (container *Container) GetResourcePath(path string) (string, error) { // IMPORTANT - These are paths on the OS where the daemon is running, hence // any filepath operations must be done in an OS agnostic way. - - cleanPath := cleanResourcePath(path) - r, e := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, cleanPath), container.BaseFS) + r, e := container.BaseFS.ResolveScopedPath(path, false) // Log this here on the daemon side as there's otherwise no indication apart // from the error being propagated all the way back to the client. This makes // debugging significantly easier and clearly indicates the error comes from the daemon. if e != nil { - logrus.Errorf("Failed to FollowSymlinkInScope BaseFS %s cleanPath %s path %s %s\n", container.BaseFS, cleanPath, path, e) + logrus.Errorf("Failed to ResolveScopedPath BaseFS %s path %s %s\n", container.BaseFS.Path(), path, e) } return r, e } diff --git a/container/container_unix.go b/container/container_unix.go index 8212cb9d7c..34638df737 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -5,7 +5,6 @@ package container import ( "io/ioutil" "os" - "path/filepath" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" @@ -13,7 +12,6 @@ import ( "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" "github.com/docker/docker/volume" "github.com/opencontainers/selinux/go-selinux/label" @@ -130,7 +128,7 @@ func (container *Container) NetworkMounts() []Mount { // CopyImagePathContent copies files in destination to the volume. func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error { - rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS) + rootfs, err := container.GetResourcePath(destination) if err != nil { return err } @@ -453,11 +451,6 @@ func (container *Container) TmpfsMounts() ([]Mount, error) { return mounts, nil } -// cleanResourcePath cleans a resource path and prepares to combine with mnt path -func cleanResourcePath(path string) string { - return filepath.Join(string(os.PathSeparator), path) -} - // EnableServiceDiscoveryOnDefaultNetwork Enable service discovery on default network func (container *Container) EnableServiceDiscoveryOnDefaultNetwork() bool { return false diff --git a/container/container_windows.go b/container/container_windows.go index 9c52d00a46..2dbea5905e 100644 --- a/container/container_windows.go +++ b/container/container_windows.go @@ -172,18 +172,6 @@ func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfi return nil } -// cleanResourcePath cleans a resource path by removing C:\ syntax, and prepares -// to combine with a volume path -func cleanResourcePath(path string) string { - if len(path) >= 2 { - c := path[0] - if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { - path = path[2:] - } - } - return filepath.Join(string(os.PathSeparator), path) -} - // BuildHostnameFile writes the container's hostname file. func (container *Container) BuildHostnameFile() error { return nil diff --git a/daemon/archive.go b/daemon/archive.go index 4bcf8d0a0c..c52d3b8509 100644 --- a/daemon/archive.go +++ b/daemon/archive.go @@ -3,7 +3,6 @@ package daemon import ( "io" "os" - "path/filepath" "strings" "github.com/docker/docker/api/types" @@ -20,6 +19,31 @@ import ( // path does not refer to a directory. var ErrExtractPointNotDirectory = errors.New("extraction point is not a directory") +// The daemon will use the following interfaces if the container fs implements +// these for optimized copies to and from the container. +type extractor interface { + ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error +} + +type archiver interface { + ArchivePath(src string, opts *archive.TarOptions) (io.ReadCloser, error) +} + +// helper functions to extract or archive +func extractArchive(i interface{}, src io.Reader, dst string, opts *archive.TarOptions) error { + if ea, ok := i.(extractor); ok { + return ea.ExtractArchive(src, dst, opts) + } + return chrootarchive.Untar(src, dst, opts) +} + +func archivePath(i interface{}, src string, opts *archive.TarOptions) (io.ReadCloser, error) { + if ap, ok := i.(archiver); ok { + return ap.ArchivePath(src, opts) + } + return archive.TarWithOptions(src, opts) +} + // ContainerCopy performs a deprecated operation of archiving the resource at // the specified path in the container identified by the given name. func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, error) { @@ -138,6 +162,9 @@ func (daemon *Daemon) containerStatPath(container *container.Container, path str return nil, err } + // Normalize path before sending to rootfs + path = container.BaseFS.FromSlash(path) + resolvedPath, absPath, err := container.ResolvePath(path) if err != nil { return nil, err @@ -178,6 +205,9 @@ func (daemon *Daemon) containerArchivePath(container *container.Container, path return nil, nil, err } + // Normalize path before sending to rootfs + path = container.BaseFS.FromSlash(path) + resolvedPath, absPath, err := container.ResolvePath(path) if err != nil { return nil, nil, err @@ -196,7 +226,18 @@ func (daemon *Daemon) containerArchivePath(container *container.Container, path // also catches the case when the root directory of the container is // requested: we want the archive entries to start with "/" and not the // container ID. - data, err := archive.TarResourceRebase(resolvedPath, filepath.Base(absPath)) + driver := container.BaseFS + + // Get the source and the base paths of the container resolved path in order + // to get the proper tar options for the rebase tar. + resolvedPath = driver.Clean(resolvedPath) + if driver.Base(resolvedPath) == "." { + resolvedPath += string(driver.Separator()) + "." + } + sourceDir, sourceBase := driver.Dir(resolvedPath), driver.Base(resolvedPath) + opts := archive.TarResourceRebaseOpts(sourceBase, driver.Base(absPath)) + + data, err := archivePath(driver, sourceDir, opts) if err != nil { return nil, nil, err } @@ -235,8 +276,12 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path return err } + // Normalize path before sending to rootfs' + path = container.BaseFS.FromSlash(path) + driver := container.BaseFS + // Check if a drive letter supplied, it must be the system drive. No-op except on Windows - path, err = system.CheckSystemDriveAndRemoveDriveLetter(path) + path, err = system.CheckSystemDriveAndRemoveDriveLetter(path, driver) if err != nil { return err } @@ -248,7 +293,10 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path // that you can extract an archive to a symlink that points to a directory. // Consider the given path as an absolute path in the container. - absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path) + absPath := archive.PreserveTrailingDotOrSeparator( + driver.Join(string(driver.Separator()), path), + path, + driver.Separator()) // This will evaluate the last path element if it is a symlink. resolvedPath, err := container.GetResourcePath(absPath) @@ -256,7 +304,7 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path return err } - stat, err := os.Lstat(resolvedPath) + stat, err := driver.Lstat(resolvedPath) if err != nil { return err } @@ -279,21 +327,24 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path // a volume file path. var baseRel string if strings.HasPrefix(resolvedPath, `\\?\Volume{`) { - if strings.HasPrefix(resolvedPath, container.BaseFS) { - baseRel = resolvedPath[len(container.BaseFS):] + if strings.HasPrefix(resolvedPath, driver.Path()) { + baseRel = resolvedPath[len(driver.Path()):] if baseRel[:1] == `\` { baseRel = baseRel[1:] } } } else { - baseRel, err = filepath.Rel(container.BaseFS, resolvedPath) + baseRel, err = driver.Rel(driver.Path(), resolvedPath) } if err != nil { return err } // Make it an absolute path. - absPath = filepath.Join(string(filepath.Separator), baseRel) + absPath = driver.Join(string(driver.Separator()), baseRel) + // @ TODO: gupta-ak: Technically, this works since it no-ops + // on Windows and the file system is local anyway on linux. + // But eventually, it should be made driver aware. toVolume, err := checkIfPathIsInAVolume(container, absPath) if err != nil { return err @@ -315,7 +366,7 @@ func (daemon *Daemon) containerExtractToDir(container *container.Container, path } } - if err := chrootarchive.Untar(content, resolvedPath, options); err != nil { + if err := extractArchive(driver, content, resolvedPath, options); err != nil { return err } @@ -356,24 +407,28 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str return nil, err } + // Normalize path before sending to rootfs + resource = container.BaseFS.FromSlash(resource) + driver := container.BaseFS + basePath, err := container.GetResourcePath(resource) if err != nil { return nil, err } - stat, err := os.Stat(basePath) + stat, err := driver.Stat(basePath) if err != nil { return nil, err } var filter []string if !stat.IsDir() { - d, f := filepath.Split(basePath) + d, f := driver.Split(basePath) basePath = d filter = []string{f} } else { - filter = []string{filepath.Base(basePath)} - basePath = filepath.Dir(basePath) + filter = []string{driver.Base(basePath)} + basePath = driver.Dir(basePath) } - archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{ + archive, err := archivePath(driver, basePath, &archive.TarOptions{ Compression: archive.Uncompressed, IncludeFiles: filter, }) diff --git a/daemon/build.go b/daemon/build.go index 39269ab5a2..be344062ff 100644 --- a/daemon/build.go +++ b/daemon/build.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/builder" "github.com/docker/docker/image" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/registry" @@ -25,9 +26,9 @@ type releaseableLayer struct { rwLayer layer.RWLayer } -func (rl *releaseableLayer) Mount() (string, error) { +func (rl *releaseableLayer) Mount() (containerfs.ContainerFS, error) { var err error - var mountPath string + var mountPath containerfs.ContainerFS var chainID layer.ChainID if rl.roLayer != nil { chainID = rl.roLayer.ChainID() @@ -36,7 +37,7 @@ func (rl *releaseableLayer) Mount() (string, error) { mountID := stringid.GenerateRandomID() rl.rwLayer, err = rl.layerStore.CreateRWLayer(mountID, chainID, nil) if err != nil { - return "", errors.Wrap(err, "failed to create rwlayer") + return nil, errors.Wrap(err, "failed to create rwlayer") } mountPath, err = rl.rwLayer.Mount("") @@ -48,7 +49,7 @@ func (rl *releaseableLayer) Mount() (string, error) { logrus.Errorf("Failed to release RWLayer: %s", err) } rl.rwLayer = nil - return "", err + return nil, err } return mountPath, nil diff --git a/daemon/daemon.go b/daemon/daemon.go index eb396ef5a4..6eb0c5c161 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -40,6 +40,7 @@ import ( "github.com/docker/docker/layer" "github.com/docker/docker/libcontainerd" "github.com/docker/docker/migrate/v1" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/plugingetter" "github.com/docker/docker/pkg/sysinfo" @@ -966,11 +967,11 @@ func (daemon *Daemon) Mount(container *container.Container) error { } logrus.Debugf("container mounted via layerStore: %v", dir) - if container.BaseFS != dir { + if container.BaseFS != nil && container.BaseFS.Path() != dir.Path() { // The mount path reported by the graph driver should always be trusted on Windows, since the // volume path for a given mounted layer may change over time. This should only be an error // on non-Windows operating systems. - if container.BaseFS != "" && runtime.GOOS != "windows" { + if runtime.GOOS != "windows" { daemon.Unmount(container) return fmt.Errorf("Error: driver %s is returning inconsistent paths for container %s ('%s' then '%s')", daemon.GraphDriverName(container.Platform), container.ID, container.BaseFS, dir) @@ -1045,7 +1046,7 @@ func prepareTempDir(rootDir string, rootIDs idtools.IDPair) (string, error) { return tmpDir, idtools.MkdirAllAndChown(tmpDir, 0700, rootIDs) } -func (daemon *Daemon) setupInitLayer(initPath string) error { +func (daemon *Daemon) setupInitLayer(initPath containerfs.ContainerFS) error { rootIDs := daemon.idMappings.RootPair() return initlayer.Setup(initPath, rootIDs) } diff --git a/daemon/daemon_solaris.go b/daemon/daemon_solaris.go index 156d11194a..9c7d67f0bd 100644 --- a/daemon/daemon_solaris.go +++ b/daemon/daemon_solaris.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/container" "github.com/docker/docker/daemon/config" "github.com/docker/docker/image" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/parsers/kernel" @@ -97,7 +98,7 @@ func setupDaemonRoot(config *config.Config, rootDir string, rootIDs idtools.IDPa return nil } -func (daemon *Daemon) getLayerInit() func(string) error { +func (daemon *Daemon) getLayerInit() func(containerfs.ContainerFS) error { return nil } diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 0082706b6c..52d4bbda89 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -24,6 +24,7 @@ import ( "github.com/docker/docker/daemon/config" "github.com/docker/docker/image" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/parsers/kernel" @@ -987,7 +988,7 @@ func removeDefaultBridgeInterface() { } } -func (daemon *Daemon) getLayerInit() func(string) error { +func (daemon *Daemon) getLayerInit() func(containerfs.ContainerFS) error { return daemon.setupInitLayer } diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go index f78f60a0af..c85a1483f2 100644 --- a/daemon/daemon_windows.go +++ b/daemon/daemon_windows.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/container" "github.com/docker/docker/daemon/config" "github.com/docker/docker/image" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/parsers" @@ -56,7 +57,7 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos return nil } -func (daemon *Daemon) getLayerInit() func(string) error { +func (daemon *Daemon) getLayerInit() func(containerfs.ContainerFS) error { return nil } diff --git a/daemon/export.go b/daemon/export.go index 081e1639b7..730387d76c 100644 --- a/daemon/export.go +++ b/daemon/export.go @@ -40,7 +40,7 @@ func (daemon *Daemon) containerExport(container *container.Container) (io.ReadCl return nil, err } - archive, err := archive.TarWithOptions(container.BaseFS, &archive.TarOptions{ + archive, err := archivePath(container.BaseFS, container.BaseFS.Path(), &archive.TarOptions{ Compression: archive.Uncompressed, UIDMaps: daemon.idMappings.UIDs(), GIDMaps: daemon.idMappings.GIDs(), diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go index 05822a70c9..8313263ff1 100644 --- a/daemon/graphdriver/aufs/aufs.go +++ b/daemon/graphdriver/aufs/aufs.go @@ -38,6 +38,7 @@ import ( "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/directory" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/locker" @@ -388,12 +389,12 @@ func atomicRemove(source string) error { // Get returns the rootfs path for the id. // This will mount the dir at its given path -func (a *Driver) Get(id, mountLabel string) (string, error) { +func (a *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { a.locker.Lock(id) defer a.locker.Unlock(id) parents, err := a.getParentLayerPaths(id) if err != nil && !os.IsNotExist(err) { - return "", err + return nil, err } a.pathCacheLock.Lock() @@ -407,21 +408,21 @@ func (a *Driver) Get(id, mountLabel string) (string, error) { } } if count := a.ctr.Increment(m); count > 1 { - return m, nil + return containerfs.NewLocalContainerFS(m), nil } // If a dir does not have a parent ( no layers )do not try to mount // just return the diff path to the data if len(parents) > 0 { if err := a.mount(id, m, mountLabel, parents); err != nil { - return "", err + return nil, err } } a.pathCacheLock.Lock() a.pathCache[id] = m a.pathCacheLock.Unlock() - return m, nil + return containerfs.NewLocalContainerFS(m), nil } // Put unmounts and updates list of active mounts. diff --git a/daemon/graphdriver/aufs/aufs_test.go b/daemon/graphdriver/aufs/aufs_test.go index 31f88e729f..a58f18a0dc 100644 --- a/daemon/graphdriver/aufs/aufs_test.go +++ b/daemon/graphdriver/aufs/aufs_test.go @@ -9,11 +9,10 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "sync" "testing" - "path/filepath" - "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/reexec" @@ -43,6 +42,14 @@ func testInit(dir string, t testing.TB) graphdriver.Driver { return d } +func driverGet(d *Driver, id string, mntLabel string) (string, error) { + mnt, err := d.Get(id, mntLabel) + if err != nil { + return "", err + } + return mnt.Path(), nil +} + func newDriver(t testing.TB) *Driver { if err := os.MkdirAll(tmp, 0755); err != nil { t.Fatal(err) @@ -172,7 +179,7 @@ func TestGetWithoutParent(t *testing.T) { t.Fatal(err) } expected := path.Join(tmp, "diff", "1") - if diffPath != expected { + if diffPath.Path() != expected { t.Fatalf("Expected path %s got %s", expected, diffPath) } } @@ -249,13 +256,13 @@ func TestMountWithParent(t *testing.T) { if err != nil { t.Fatal(err) } - if mntPath == "" { - t.Fatal("mntPath should not be empty string") + if mntPath == nil { + t.Fatal("mntPath should not be nil") } expected := path.Join(tmp, "mnt", "2") - if mntPath != expected { - t.Fatalf("Expected %s got %s", expected, mntPath) + if mntPath.Path() != expected { + t.Fatalf("Expected %s got %s", expected, mntPath.Path()) } } @@ -280,8 +287,8 @@ func TestRemoveMountedDir(t *testing.T) { if err != nil { t.Fatal(err) } - if mntPath == "" { - t.Fatal("mntPath should not be empty string") + if mntPath == nil { + t.Fatal("mntPath should not be nil") } mounted, err := d.mounted(d.pathCache["2"]) @@ -315,7 +322,7 @@ func TestGetDiff(t *testing.T) { t.Fatal(err) } - diffPath, err := d.Get("1", "") + diffPath, err := driverGet(d, "1", "") if err != nil { t.Fatal(err) } @@ -359,7 +366,7 @@ func TestChanges(t *testing.T) { } }() - mntPoint, err := d.Get("2", "") + mntPoint, err := driverGet(d, "2", "") if err != nil { t.Fatal(err) } @@ -398,7 +405,7 @@ func TestChanges(t *testing.T) { if err := d.CreateReadWrite("3", "2", nil); err != nil { t.Fatal(err) } - mntPoint, err = d.Get("3", "") + mntPoint, err = driverGet(d, "3", "") if err != nil { t.Fatal(err) } @@ -444,7 +451,7 @@ func TestDiffSize(t *testing.T) { t.Fatal(err) } - diffPath, err := d.Get("1", "") + diffPath, err := driverGet(d, "1", "") if err != nil { t.Fatal(err) } @@ -486,7 +493,7 @@ func TestChildDiffSize(t *testing.T) { t.Fatal(err) } - diffPath, err := d.Get("1", "") + diffPath, err := driverGet(d, "1", "") if err != nil { t.Fatal(err) } @@ -587,7 +594,7 @@ func TestApplyDiff(t *testing.T) { t.Fatal(err) } - diffPath, err := d.Get("1", "") + diffPath, err := driverGet(d, "1", "") if err != nil { t.Fatal(err) } @@ -622,7 +629,7 @@ func TestApplyDiff(t *testing.T) { // Ensure that the file is in the mount point for id 3 - mountPoint, err := d.Get("3", "") + mountPoint, err := driverGet(d, "3", "") if err != nil { t.Fatal(err) } @@ -665,7 +672,7 @@ func testMountMoreThan42Layers(t *testing.T, mountPath string) { err := d.CreateReadWrite(current, parent, nil) require.NoError(t, err, "current layer %d", i) - point, err := d.Get(current, "") + point, err := driverGet(d, current, "") require.NoError(t, err, "current layer %d", i) f, err := os.Create(path.Join(point, current)) @@ -681,7 +688,7 @@ func testMountMoreThan42Layers(t *testing.T, mountPath string) { } // Perform the actual mount for the top most image - point, err := d.Get(last, "") + point, err := driverGet(d, last, "") require.NoError(t, err) files, err := ioutil.ReadDir(point) require.NoError(t, err) diff --git a/daemon/graphdriver/btrfs/btrfs.go b/daemon/graphdriver/btrfs/btrfs.go index dcdfc9a867..5a61217341 100644 --- a/daemon/graphdriver/btrfs/btrfs.go +++ b/daemon/graphdriver/btrfs/btrfs.go @@ -27,6 +27,7 @@ import ( "unsafe" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/parsers" @@ -631,29 +632,29 @@ func (d *Driver) Remove(id string) error { } // Get the requested filesystem id. -func (d *Driver) Get(id, mountLabel string) (string, error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { dir := d.subvolumesDirID(id) st, err := os.Stat(dir) if err != nil { - return "", err + return nil, err } if !st.IsDir() { - return "", fmt.Errorf("%s: not a directory", dir) + return nil, fmt.Errorf("%s: not a directory", dir) } if quota, err := ioutil.ReadFile(d.quotasDirID(id)); err == nil { if size, err := strconv.ParseUint(string(quota), 10, 64); err == nil && size >= d.options.minSpace { if err := d.subvolEnableQuota(); err != nil { - return "", err + return nil, err } if err := subvolLimitQgroup(dir, size); err != nil { - return "", err + return nil, err } } } - return dir, nil + return containerfs.NewLocalContainerFS(dir), nil } // Put is not implemented for BTRFS as there is no cleanup required for the id. diff --git a/daemon/graphdriver/btrfs/btrfs_test.go b/daemon/graphdriver/btrfs/btrfs_test.go index 0038dbcdcd..056b99b94b 100644 --- a/daemon/graphdriver/btrfs/btrfs_test.go +++ b/daemon/graphdriver/btrfs/btrfs_test.go @@ -35,12 +35,14 @@ func TestBtrfsSubvolDelete(t *testing.T) { } defer graphtest.PutDriver(t) - dir, err := d.Get("test", "") + dirFS, err := d.Get("test", "") if err != nil { t.Fatal(err) } defer d.Put("test") + dir := dirFS.Path() + if err := subvolCreate(dir, "subvoltest"); err != nil { t.Fatal(err) } diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go index c7756ecc3b..f41afa2ae7 100644 --- a/daemon/graphdriver/devmapper/driver.go +++ b/daemon/graphdriver/devmapper/driver.go @@ -12,6 +12,7 @@ import ( "github.com/sirupsen/logrus" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/devicemapper" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/locker" @@ -163,41 +164,41 @@ func (d *Driver) Remove(id string) error { } // Get mounts a device with given id into the root filesystem -func (d *Driver) Get(id, mountLabel string) (string, error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { d.locker.Lock(id) defer d.locker.Unlock(id) mp := path.Join(d.home, "mnt", id) rootFs := path.Join(mp, "rootfs") if count := d.ctr.Increment(mp); count > 1 { - return rootFs, nil + return containerfs.NewLocalContainerFS(rootFs), nil } uid, gid, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) if err != nil { d.ctr.Decrement(mp) - return "", err + return nil, err } // Create the target directories if they don't exist if err := idtools.MkdirAllAs(path.Join(d.home, "mnt"), 0755, uid, gid); err != nil && !os.IsExist(err) { d.ctr.Decrement(mp) - return "", err + return nil, err } if err := idtools.MkdirAs(mp, 0755, uid, gid); err != nil && !os.IsExist(err) { d.ctr.Decrement(mp) - return "", err + return nil, err } // Mount the device if err := d.DeviceSet.MountDevice(id, mp, mountLabel); err != nil { d.ctr.Decrement(mp) - return "", err + return nil, err } if err := idtools.MkdirAllAs(rootFs, 0755, uid, gid); err != nil && !os.IsExist(err) { d.ctr.Decrement(mp) d.DeviceSet.UnmountDevice(id, mp) - return "", err + return nil, err } idFile := path.Join(mp, "id") @@ -207,11 +208,11 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { if err := ioutil.WriteFile(idFile, []byte(id), 0600); err != nil { d.ctr.Decrement(mp) d.DeviceSet.UnmountDevice(id, mp) - return "", err + return nil, err } } - return rootFs, nil + return containerfs.NewLocalContainerFS(rootFs), nil } // Put unmounts a device and removes it. diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 94c52094c3..68f9022e1c 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -12,6 +12,7 @@ import ( "github.com/vbatts/tar-split/tar/storage" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/plugingetter" ) @@ -68,7 +69,7 @@ type ProtoDriver interface { // Get returns the mountpoint for the layered filesystem referred // to by this id. You can optionally specify a mountLabel or "". // Returns the absolute path to the mounted layered filesystem. - Get(id, mountLabel string) (dir string, err error) + Get(id, mountLabel string) (fs containerfs.ContainerFS, err error) // Put releases the system resources for the specified id, // e.g, unmounting layered filesystem. Put(id string) error diff --git a/daemon/graphdriver/fsdiff.go b/daemon/graphdriver/fsdiff.go index 6f4258e8e6..533c5a7699 100644 --- a/daemon/graphdriver/fsdiff.go +++ b/daemon/graphdriver/fsdiff.go @@ -18,9 +18,9 @@ var ( ) // NaiveDiffDriver takes a ProtoDriver and adds the -// capability of the Diffing methods which it may or may not -// support on its own. See the comment on the exported -// NewNaiveDiffDriver function below. +// capability of the Diffing methods on the local file system, +// which it may or may not support on its own. See the comment +// on the exported NewNaiveDiffDriver function below. // Notably, the AUFS driver doesn't need to be wrapped like this. type NaiveDiffDriver struct { ProtoDriver @@ -47,10 +47,11 @@ func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch io.ReadCloser, err err startTime := time.Now() driver := gdw.ProtoDriver - layerFs, err := driver.Get(id, "") + layerRootFs, err := driver.Get(id, "") if err != nil { return nil, err } + layerFs := layerRootFs.Path() defer func() { if err != nil { @@ -70,12 +71,14 @@ func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch io.ReadCloser, err err }), nil } - parentFs, err := driver.Get(parent, "") + parentRootFs, err := driver.Get(parent, "") if err != nil { return nil, err } defer driver.Put(parent) + parentFs := parentRootFs.Path() + changes, err := archive.ChangesDirs(layerFs, parentFs) if err != nil { return nil, err @@ -104,20 +107,22 @@ func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch io.ReadCloser, err err func (gdw *NaiveDiffDriver) Changes(id, parent string) ([]archive.Change, error) { driver := gdw.ProtoDriver - layerFs, err := driver.Get(id, "") + layerRootFs, err := driver.Get(id, "") if err != nil { return nil, err } defer driver.Put(id) + layerFs := layerRootFs.Path() parentFs := "" if parent != "" { - parentFs, err = driver.Get(parent, "") + parentRootFs, err := driver.Get(parent, "") if err != nil { return nil, err } defer driver.Put(parent) + parentFs = parentRootFs.Path() } return archive.ChangesDirs(layerFs, parentFs) @@ -130,12 +135,13 @@ func (gdw *NaiveDiffDriver) ApplyDiff(id, parent string, diff io.Reader) (size i driver := gdw.ProtoDriver // Mount the root filesystem so we can apply the diff/layer. - layerFs, err := driver.Get(id, "") + layerRootFs, err := driver.Get(id, "") if err != nil { return } defer driver.Put(id) + layerFs := layerRootFs.Path() options := &archive.TarOptions{UIDMaps: gdw.uidMaps, GIDMaps: gdw.gidMaps} start := time.Now().UTC() @@ -165,5 +171,5 @@ func (gdw *NaiveDiffDriver) DiffSize(id, parent string) (size int64, err error) } defer driver.Put(id) - return archive.ChangesSize(layerFs, changes), nil + return archive.ChangesSize(layerFs.Path(), changes), nil } diff --git a/daemon/graphdriver/graphtest/graphbench_unix.go b/daemon/graphdriver/graphtest/graphbench_unix.go index aa833e8a83..c04394d519 100644 --- a/daemon/graphdriver/graphtest/graphbench_unix.go +++ b/daemon/graphdriver/graphtest/graphbench_unix.go @@ -5,9 +5,9 @@ package graphtest import ( "io" "io/ioutil" - "path/filepath" "testing" + contdriver "github.com/containerd/continuity/driver" "github.com/docker/docker/pkg/stringid" "github.com/stretchr/testify/require" ) @@ -245,7 +245,7 @@ func DriverBenchDeepLayerRead(b *testing.B, layerCount int, drivername string, d for i := 0; i < b.N; i++ { // Read content - c, err := ioutil.ReadFile(filepath.Join(root, "testfile.txt")) + c, err := contdriver.ReadFile(root, root.Join(root.Path(), "testfile.txt")) if err != nil { b.Fatal(err) } diff --git a/daemon/graphdriver/graphtest/graphtest_unix.go b/daemon/graphdriver/graphtest/graphtest_unix.go index 2f8ae54777..11dff48896 100644 --- a/daemon/graphdriver/graphtest/graphtest_unix.go +++ b/daemon/graphdriver/graphtest/graphtest_unix.go @@ -97,10 +97,10 @@ func DriverTestCreateEmpty(t testing.TB, drivername string, driverOptions ...str dir, err := driver.Get("empty", "") require.NoError(t, err) - verifyFile(t, dir, 0755|os.ModeDir, 0, 0) + verifyFile(t, dir.Path(), 0755|os.ModeDir, 0, 0) // Verify that the directory is empty - fis, err := readDir(dir) + fis, err := readDir(dir, dir.Path()) require.NoError(t, err) assert.Len(t, fis, 0) @@ -328,9 +328,9 @@ func DriverTestSetQuota(t *testing.T, drivername string) { } quota := uint64(50 * units.MiB) - err = writeRandomFile(path.Join(mountPath, "file"), quota*2) + + err = writeRandomFile(path.Join(mountPath.Path(), "file"), quota*2) if pathError, ok := err.(*os.PathError); ok && pathError.Err != unix.EDQUOT { t.Fatalf("expect write() to fail with %v, got %v", unix.EDQUOT, err) } - } diff --git a/daemon/graphdriver/graphtest/testutil.go b/daemon/graphdriver/graphtest/testutil.go index 40f8f554f0..946313296b 100644 --- a/daemon/graphdriver/graphtest/testutil.go +++ b/daemon/graphdriver/graphtest/testutil.go @@ -3,12 +3,11 @@ package graphtest import ( "bytes" "fmt" - "io/ioutil" "math/rand" "os" - "path" "sort" + "github.com/containerd/continuity/driver" "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/stringid" @@ -36,17 +35,17 @@ func addFiles(drv graphdriver.Driver, layer string, seed int64) error { } defer drv.Put(layer) - if err := ioutil.WriteFile(path.Join(root, "file-a"), randomContent(64, seed), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(root.Path(), "file-a"), randomContent(64, seed), 0755); err != nil { return err } - if err := os.MkdirAll(path.Join(root, "dir-b"), 0755); err != nil { + if err := root.MkdirAll(root.Join(root.Path(), "dir-b"), 0755); err != nil { return err } - if err := ioutil.WriteFile(path.Join(root, "dir-b", "file-b"), randomContent(128, seed+1), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(root.Path(), "dir-b", "file-b"), randomContent(128, seed+1), 0755); err != nil { return err } - return ioutil.WriteFile(path.Join(root, "file-c"), randomContent(128*128, seed+2), 0755) + return driver.WriteFile(root, root.Join(root.Path(), "file-c"), randomContent(128*128, seed+2), 0755) } func checkFile(drv graphdriver.Driver, layer, filename string, content []byte) error { @@ -56,7 +55,7 @@ func checkFile(drv graphdriver.Driver, layer, filename string, content []byte) e } defer drv.Put(layer) - fileContent, err := ioutil.ReadFile(path.Join(root, filename)) + fileContent, err := driver.ReadFile(root, root.Join(root.Path(), filename)) if err != nil { return err } @@ -75,7 +74,7 @@ func addFile(drv graphdriver.Driver, layer, filename string, content []byte) err } defer drv.Put(layer) - return ioutil.WriteFile(path.Join(root, filename), content, 0755) + return driver.WriteFile(root, root.Join(root.Path(), filename), content, 0755) } func addDirectory(drv graphdriver.Driver, layer, dir string) error { @@ -85,7 +84,7 @@ func addDirectory(drv graphdriver.Driver, layer, dir string) error { } defer drv.Put(layer) - return os.MkdirAll(path.Join(root, dir), 0755) + return root.MkdirAll(root.Join(root.Path(), dir), 0755) } func removeAll(drv graphdriver.Driver, layer string, names ...string) error { @@ -96,7 +95,7 @@ func removeAll(drv graphdriver.Driver, layer string, names ...string) error { defer drv.Put(layer) for _, filename := range names { - if err := os.RemoveAll(path.Join(root, filename)); err != nil { + if err := root.RemoveAll(root.Join(root.Path(), filename)); err != nil { return err } } @@ -110,8 +109,8 @@ func checkFileRemoved(drv graphdriver.Driver, layer, filename string) error { } defer drv.Put(layer) - if _, err := os.Stat(path.Join(root, filename)); err == nil { - return fmt.Errorf("file still exists: %s", path.Join(root, filename)) + if _, err := root.Stat(root.Join(root.Path(), filename)); err == nil { + return fmt.Errorf("file still exists: %s", root.Join(root.Path(), filename)) } else if !os.IsNotExist(err) { return err } @@ -127,13 +126,13 @@ func addManyFiles(drv graphdriver.Driver, layer string, count int, seed int64) e defer drv.Put(layer) for i := 0; i < count; i += 100 { - dir := path.Join(root, fmt.Sprintf("directory-%d", i)) - if err := os.MkdirAll(dir, 0755); err != nil { + dir := root.Join(root.Path(), fmt.Sprintf("directory-%d", i)) + if err := root.MkdirAll(dir, 0755); err != nil { return err } for j := 0; i+j < count && j < 100; j++ { - file := path.Join(dir, fmt.Sprintf("file-%d", i+j)) - if err := ioutil.WriteFile(file, randomContent(64, seed+int64(i+j)), 0755); err != nil { + file := root.Join(dir, fmt.Sprintf("file-%d", i+j)) + if err := driver.WriteFile(root, file, randomContent(64, seed+int64(i+j)), 0755); err != nil { return err } } @@ -152,7 +151,7 @@ func changeManyFiles(drv graphdriver.Driver, layer string, count int, seed int64 changes := []archive.Change{} for i := 0; i < count; i += 100 { archiveRoot := fmt.Sprintf("/directory-%d", i) - if err := os.MkdirAll(path.Join(root, archiveRoot), 0755); err != nil { + if err := root.MkdirAll(root.Join(root.Path(), archiveRoot), 0755); err != nil { return nil, err } for j := 0; i+j < count && j < 100; j++ { @@ -166,23 +165,23 @@ func changeManyFiles(drv graphdriver.Driver, layer string, count int, seed int64 switch j % 3 { // Update file case 0: - change.Path = path.Join(archiveRoot, fmt.Sprintf("file-%d", i+j)) + change.Path = root.Join(archiveRoot, fmt.Sprintf("file-%d", i+j)) change.Kind = archive.ChangeModify - if err := ioutil.WriteFile(path.Join(root, change.Path), randomContent(64, seed+int64(i+j)), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(root.Path(), change.Path), randomContent(64, seed+int64(i+j)), 0755); err != nil { return nil, err } // Add file case 1: - change.Path = path.Join(archiveRoot, fmt.Sprintf("file-%d-%d", seed, i+j)) + change.Path = root.Join(archiveRoot, fmt.Sprintf("file-%d-%d", seed, i+j)) change.Kind = archive.ChangeAdd - if err := ioutil.WriteFile(path.Join(root, change.Path), randomContent(64, seed+int64(i+j)), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(root.Path(), change.Path), randomContent(64, seed+int64(i+j)), 0755); err != nil { return nil, err } // Remove file case 2: - change.Path = path.Join(archiveRoot, fmt.Sprintf("file-%d", i+j)) + change.Path = root.Join(archiveRoot, fmt.Sprintf("file-%d", i+j)) change.Kind = archive.ChangeDelete - if err := os.Remove(path.Join(root, change.Path)); err != nil { + if err := root.Remove(root.Join(root.Path(), change.Path)); err != nil { return nil, err } } @@ -201,10 +200,10 @@ func checkManyFiles(drv graphdriver.Driver, layer string, count int, seed int64) defer drv.Put(layer) for i := 0; i < count; i += 100 { - dir := path.Join(root, fmt.Sprintf("directory-%d", i)) + dir := root.Join(root.Path(), fmt.Sprintf("directory-%d", i)) for j := 0; i+j < count && j < 100; j++ { - file := path.Join(dir, fmt.Sprintf("file-%d", i+j)) - fileContent, err := ioutil.ReadFile(file) + file := root.Join(dir, fmt.Sprintf("file-%d", i+j)) + fileContent, err := driver.ReadFile(root, file) if err != nil { return err } @@ -254,17 +253,17 @@ func addLayerFiles(drv graphdriver.Driver, layer, parent string, i int) error { } defer drv.Put(layer) - if err := ioutil.WriteFile(path.Join(root, "top-id"), []byte(layer), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(root.Path(), "top-id"), []byte(layer), 0755); err != nil { return err } - layerDir := path.Join(root, fmt.Sprintf("layer-%d", i)) - if err := os.MkdirAll(layerDir, 0755); err != nil { + layerDir := root.Join(root.Path(), fmt.Sprintf("layer-%d", i)) + if err := root.MkdirAll(layerDir, 0755); err != nil { return err } - if err := ioutil.WriteFile(path.Join(layerDir, "layer-id"), []byte(layer), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(layerDir, "layer-id"), []byte(layer), 0755); err != nil { return err } - if err := ioutil.WriteFile(path.Join(layerDir, "parent-id"), []byte(parent), 0755); err != nil { + if err := driver.WriteFile(root, root.Join(layerDir, "parent-id"), []byte(parent), 0755); err != nil { return err } @@ -295,7 +294,7 @@ func checkManyLayers(drv graphdriver.Driver, layer string, count int) error { } defer drv.Put(layer) - layerIDBytes, err := ioutil.ReadFile(path.Join(root, "top-id")) + layerIDBytes, err := driver.ReadFile(root, root.Join(root.Path(), "top-id")) if err != nil { return err } @@ -305,16 +304,16 @@ func checkManyLayers(drv graphdriver.Driver, layer string, count int) error { } for i := count; i > 0; i-- { - layerDir := path.Join(root, fmt.Sprintf("layer-%d", i)) + layerDir := root.Join(root.Path(), fmt.Sprintf("layer-%d", i)) - thisLayerIDBytes, err := ioutil.ReadFile(path.Join(layerDir, "layer-id")) + thisLayerIDBytes, err := driver.ReadFile(root, root.Join(layerDir, "layer-id")) if err != nil { return err } if !bytes.Equal(thisLayerIDBytes, layerIDBytes) { return fmt.Errorf("mismatched file content %v, expecting %v", thisLayerIDBytes, layerIDBytes) } - layerIDBytes, err = ioutil.ReadFile(path.Join(layerDir, "parent-id")) + layerIDBytes, err = driver.ReadFile(root, root.Join(layerDir, "parent-id")) if err != nil { return err } @@ -322,11 +321,11 @@ func checkManyLayers(drv graphdriver.Driver, layer string, count int) error { return nil } -// readDir reads a directory just like ioutil.ReadDir() +// readDir reads a directory just like driver.ReadDir() // then hides specific files (currently "lost+found") // so the tests don't "see" it -func readDir(dir string) ([]os.FileInfo, error) { - a, err := ioutil.ReadDir(dir) +func readDir(r driver.Driver, dir string) ([]os.FileInfo, error) { + a, err := driver.ReadDir(r, dir) if err != nil { return nil, err } diff --git a/daemon/graphdriver/graphtest/testutil_unix.go b/daemon/graphdriver/graphtest/testutil_unix.go index 96474487aa..b5dec43fb3 100644 --- a/daemon/graphdriver/graphtest/testutil_unix.go +++ b/daemon/graphdriver/graphtest/testutil_unix.go @@ -3,12 +3,11 @@ package graphtest import ( - "io/ioutil" "os" - "path" "syscall" "testing" + contdriver "github.com/containerd/continuity/driver" "github.com/docker/docker/daemon/graphdriver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,31 +39,31 @@ func createBase(t testing.TB, driver graphdriver.Driver, name string) { err := driver.CreateReadWrite(name, "", nil) require.NoError(t, err) - dir, err := driver.Get(name, "") + dirFS, err := driver.Get(name, "") require.NoError(t, err) defer driver.Put(name) - subdir := path.Join(dir, "a subdir") - require.NoError(t, os.Mkdir(subdir, 0705|os.ModeSticky)) - require.NoError(t, os.Chown(subdir, 1, 2)) + subdir := dirFS.Join(dirFS.Path(), "a subdir") + require.NoError(t, dirFS.Mkdir(subdir, 0705|os.ModeSticky)) + require.NoError(t, dirFS.Lchown(subdir, 1, 2)) - file := path.Join(dir, "a file") - err = ioutil.WriteFile(file, []byte("Some data"), 0222|os.ModeSetuid) + file := dirFS.Join(dirFS.Path(), "a file") + err = contdriver.WriteFile(dirFS, file, []byte("Some data"), 0222|os.ModeSetuid) require.NoError(t, err) } func verifyBase(t testing.TB, driver graphdriver.Driver, name string) { - dir, err := driver.Get(name, "") + dirFS, err := driver.Get(name, "") require.NoError(t, err) defer driver.Put(name) - subdir := path.Join(dir, "a subdir") + subdir := dirFS.Join(dirFS.Path(), "a subdir") verifyFile(t, subdir, 0705|os.ModeDir|os.ModeSticky, 1, 2) - file := path.Join(dir, "a file") + file := dirFS.Join(dirFS.Path(), "a file") verifyFile(t, file, 0222|os.ModeSetuid, 0, 0) - files, err := readDir(dir) + files, err := readDir(dirFS, dirFS.Path()) require.NoError(t, err) assert.Len(t, files, 2) } diff --git a/daemon/graphdriver/lcow/lcow.go b/daemon/graphdriver/lcow/lcow.go index 3b6e1d9ef5..5f71b7337e 100644 --- a/daemon/graphdriver/lcow/lcow.go +++ b/daemon/graphdriver/lcow/lcow.go @@ -65,12 +65,14 @@ import ( "strconv" "strings" "sync" + "syscall" "time" "github.com/Microsoft/hcsshim" "github.com/Microsoft/opengcs/client" "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/system" @@ -106,72 +108,24 @@ const ( // scratchDirectory is the sub-folder under the driver's data-root used for scratch VHDs in service VMs scratchDirectory = "scratch" + + // errOperationPending is the HRESULT returned by the HCS when the VM termination operation is still pending. + errOperationPending syscall.Errno = 0xc0370103 ) -// cacheItem is our internal structure representing an item in our local cache -// of things that have been mounted. -type cacheItem struct { - sync.Mutex // Protects operations performed on this item - uvmPath string // Path in utility VM - hostPath string // Path on host - refCount int // How many times its been mounted - isSandbox bool // True if a sandbox - isMounted bool // True when mounted in a service VM -} - -// setIsMounted is a helper function for a cacheItem which does exactly what it says -func (ci *cacheItem) setIsMounted() { - logrus.Debugf("locking cache item for set isMounted") - ci.Lock() - defer ci.Unlock() - ci.isMounted = true - logrus.Debugf("set isMounted on cache item") -} - -// incrementRefCount is a helper function for a cacheItem which does exactly what it says -func (ci *cacheItem) incrementRefCount() { - logrus.Debugf("locking cache item for increment") - ci.Lock() - defer ci.Unlock() - ci.refCount++ - logrus.Debugf("incremented refcount on cache item %+v", ci) -} - -// decrementRefCount is a helper function for a cacheItem which does exactly what it says -func (ci *cacheItem) decrementRefCount() int { - logrus.Debugf("locking cache item for decrement") - ci.Lock() - defer ci.Unlock() - ci.refCount-- - logrus.Debugf("decremented refcount on cache item %+v", ci) - return ci.refCount -} - -// serviceVMItem is our internal structure representing an item in our -// map of service VMs we are maintaining. -type serviceVMItem struct { - sync.Mutex // Serialises operations being performed in this service VM. - scratchAttached bool // Has a scratch been attached? - config *client.Config // Represents the service VM item. -} - // Driver represents an LCOW graph driver. type Driver struct { - dataRoot string // Root path on the host where we are storing everything. - cachedSandboxFile string // Location of the local default-sized cached sandbox. - cachedSandboxMutex sync.Mutex // Protects race conditions from multiple threads creating the cached sandbox. - cachedScratchFile string // Location of the local cached empty scratch space. - cachedScratchMutex sync.Mutex // Protects race conditions from multiple threads creating the cached scratch. - options []string // Graphdriver options we are initialised with. - serviceVmsMutex sync.Mutex // Protects add/updates/delete to the serviceVMs map. - serviceVms map[string]*serviceVMItem // Map of the configs representing the service VM(s) we are running. - globalMode bool // Indicates if running in an unsafe/global service VM mode. + dataRoot string // Root path on the host where we are storing everything. + cachedSandboxFile string // Location of the local default-sized cached sandbox. + cachedSandboxMutex sync.Mutex // Protects race conditions from multiple threads creating the cached sandbox. + cachedScratchFile string // Location of the local cached empty scratch space. + cachedScratchMutex sync.Mutex // Protects race conditions from multiple threads creating the cached scratch. + options []string // Graphdriver options we are initialised with. + globalMode bool // Indicates if running in an unsafe/global service VM mode. // NOTE: It is OK to use a cache here because Windows does not support // restoring containers when the daemon dies. - - cacheMutex sync.Mutex // Protects add/update/deletes to cache. - cache map[string]*cacheItem // Map holding a cache of all the IDs we've mounted/unmounted. + serviceVms *serviceVMMap // Map of the configs representing the service VM(s) we are running. } // layerDetails is the structure returned by a helper function `getLayerDetails` @@ -204,9 +158,10 @@ func InitDriver(dataRoot string, options []string, _, _ []idtools.IDMap) (graphd options: options, cachedSandboxFile: filepath.Join(cd, sandboxFilename), cachedScratchFile: filepath.Join(cd, scratchFilename), - cache: make(map[string]*cacheItem), - serviceVms: make(map[string]*serviceVMItem), - globalMode: false, + serviceVms: &serviceVMMap{ + svms: make(map[string]*serviceVMMapItem), + }, + globalMode: false, } // Looks for relevant options @@ -248,53 +203,59 @@ func InitDriver(dataRoot string, options []string, _, _ []idtools.IDMap) (graphd return d, nil } +func (d *Driver) getVMID(id string) string { + if d.globalMode { + return svmGlobalID + } + return id +} + // startServiceVMIfNotRunning starts a service utility VM if it is not currently running. // It can optionally be started with a mapped virtual disk. Returns a opengcs config structure // representing the VM. -func (d *Driver) startServiceVMIfNotRunning(id string, mvdToAdd *hcsshim.MappedVirtualDisk, context string) (*serviceVMItem, error) { +func (d *Driver) startServiceVMIfNotRunning(id string, mvdToAdd []hcsshim.MappedVirtualDisk, context string) (_ *serviceVM, err error) { // Use the global ID if in global mode - if d.globalMode { - id = svmGlobalID - } + id = d.getVMID(id) title := fmt.Sprintf("lcowdriver: startservicevmifnotrunning %s:", id) - // Make sure thread-safe when interrogating the map - logrus.Debugf("%s taking serviceVmsMutex", title) - d.serviceVmsMutex.Lock() + // Attempt to add ID to the service vm map + logrus.Debugf("%s: Adding entry to service vm map", title) + svm, exists, err := d.serviceVms.add(id) + if err != nil && err == errVMisTerminating { + // VM is in the process of terminating. Wait until it's done and and then try again + logrus.Debugf("%s: VM with current ID still in the process of terminating: %s", title, id) + if err := svm.getStopError(); err != nil { + logrus.Debugf("%s: VM %s did not stop succesfully: %s", title, id, err) + return nil, err + } + return d.startServiceVMIfNotRunning(id, mvdToAdd, context) + } else if err != nil { + logrus.Debugf("%s: failed to add service vm to map: %s", err) + return nil, fmt.Errorf("%s: failed to add to service vm map: %s", title, err) + } - // Nothing to do if it's already running except add the mapped drive if supplied. - if svm, ok := d.serviceVms[id]; ok { - logrus.Debugf("%s exists, releasing serviceVmsMutex", title) - d.serviceVmsMutex.Unlock() - - if mvdToAdd != nil { - logrus.Debugf("hot-adding %s to %s", mvdToAdd.HostPath, mvdToAdd.ContainerPath) - - // Ensure the item is locked while doing this - logrus.Debugf("%s locking serviceVmItem %s", title, svm.config.Name) - svm.Lock() - - if err := svm.config.HotAddVhd(mvdToAdd.HostPath, mvdToAdd.ContainerPath, false, true); err != nil { - logrus.Debugf("%s releasing serviceVmItem %s on hot-add failure %s", title, svm.config.Name, err) - svm.Unlock() - return nil, fmt.Errorf("%s hot add %s to %s failed: %s", title, mvdToAdd.HostPath, mvdToAdd.ContainerPath, err) - } - - logrus.Debugf("%s releasing serviceVmItem %s", title, svm.config.Name) - svm.Unlock() + if exists { + // Service VM is already up and running. In this case, just hot add the vhds. + logrus.Debugf("%s: service vm already exists. Just hot adding: %+v", title, mvdToAdd) + if err := svm.hotAddVHDs(mvdToAdd...); err != nil { + logrus.Debugf("%s: failed to hot add vhds on service vm creation: %s", title, err) + return nil, fmt.Errorf("%s: failed to hot add vhds on service vm: %s", title, err) } return svm, nil } - // Release the lock early - logrus.Debugf("%s releasing serviceVmsMutex", title) - d.serviceVmsMutex.Unlock() + // We are the first service for this id, so we need to start it + logrus.Debugf("%s: service vm doesn't exist. Now starting it up: %s", title, id) - // So we are starting one. First need an enpty structure. - svm := &serviceVMItem{ - config: &client.Config{}, - } + defer func() { + // Signal that start has finished, passing in the error if any. + svm.signalStartFinished(err) + if err != nil { + // We added a ref to the VM, since we failed, we should delete the ref. + d.terminateServiceVM(id, "error path on startServiceVMIfNotRunning", false) + } + }() // Generate a default configuration if err := svm.config.GenerateDefault(d.options); err != nil { @@ -335,12 +296,14 @@ func (d *Driver) startServiceVMIfNotRunning(id string, mvdToAdd *hcsshim.MappedV svm.config.MappedVirtualDisks = append(svm.config.MappedVirtualDisks, mvd) svm.scratchAttached = true } + logrus.Debugf("%s releasing cachedScratchMutex", title) d.cachedScratchMutex.Unlock() // If requested to start it with a mapped virtual disk, add it now. - if mvdToAdd != nil { - svm.config.MappedVirtualDisks = append(svm.config.MappedVirtualDisks, *mvdToAdd) + svm.config.MappedVirtualDisks = append(svm.config.MappedVirtualDisks, mvdToAdd...) + for _, mvd := range svm.config.MappedVirtualDisks { + svm.attachedVHDs[mvd.HostPath] = 1 } // Start it. @@ -349,108 +312,80 @@ func (d *Driver) startServiceVMIfNotRunning(id string, mvdToAdd *hcsshim.MappedV return nil, fmt.Errorf("failed to start service utility VM (%s): %s", context, err) } - // As it's now running, add it to the map, checking for a race where another - // thread has simultaneously tried to start it. - logrus.Debugf("%s locking serviceVmsMutex for insertion", title) - d.serviceVmsMutex.Lock() - if svm, ok := d.serviceVms[id]; ok { - logrus.Debugf("%s releasing serviceVmsMutex after insertion but exists", title) - d.serviceVmsMutex.Unlock() - return svm, nil - } - d.serviceVms[id] = svm - logrus.Debugf("%s releasing serviceVmsMutex after insertion", title) - d.serviceVmsMutex.Unlock() + // defer function to terminate the VM if the next steps fail + defer func() { + if err != nil { + waitTerminate(svm, fmt.Sprintf("startServiceVmIfNotRunning: %s (%s)", id, context)) + } + }() // Now we have a running service VM, we can create the cached scratch file if it doesn't exist. logrus.Debugf("%s locking cachedScratchMutex", title) d.cachedScratchMutex.Lock() if _, err := os.Stat(d.cachedScratchFile); err != nil { - logrus.Debugf("%s (%s): creating an SVM scratch - locking serviceVM", title, context) - svm.Lock() + logrus.Debugf("%s (%s): creating an SVM scratch", title, context) + + // Don't use svm.CreateExt4Vhdx since that only works when the service vm is setup, + // but we're still in that process right now. if err := svm.config.CreateExt4Vhdx(scratchTargetFile, client.DefaultVhdxSizeGB, d.cachedScratchFile); err != nil { - logrus.Debugf("%s (%s): releasing serviceVM on error path from CreateExt4Vhdx: %s", title, context, err) - svm.Unlock() logrus.Debugf("%s (%s): releasing cachedScratchMutex on error path", title, context) d.cachedScratchMutex.Unlock() - - // Do a force terminate and remove it from the map on failure, ignoring any errors - if err2 := d.terminateServiceVM(id, "error path from CreateExt4Vhdx", true); err2 != nil { - logrus.Warnf("failed to terminate service VM on error path from CreateExt4Vhdx: %s", err2) - } - + logrus.Debugf("%s: failed to create vm scratch %s: %s", title, scratchTargetFile, err) return nil, fmt.Errorf("failed to create SVM scratch VHDX (%s): %s", context, err) } - logrus.Debugf("%s (%s): releasing serviceVM after %s created and cached to %s", title, context, scratchTargetFile, d.cachedScratchFile) - svm.Unlock() } logrus.Debugf("%s (%s): releasing cachedScratchMutex", title, context) d.cachedScratchMutex.Unlock() // Hot-add the scratch-space if not already attached if !svm.scratchAttached { - logrus.Debugf("lcowdriver: startServiceVmIfNotRunning: (%s) hot-adding scratch %s - locking serviceVM", context, scratchTargetFile) - svm.Lock() - if err := svm.config.HotAddVhd(scratchTargetFile, toolsScratchPath, false, true); err != nil { - logrus.Debugf("%s (%s): releasing serviceVM on error path of HotAddVhd: %s", title, context, err) - svm.Unlock() - - // Do a force terminate and remove it from the map on failure, ignoring any errors - if err2 := d.terminateServiceVM(id, "error path from HotAddVhd", true); err2 != nil { - logrus.Warnf("failed to terminate service VM on error path from HotAddVhd: %s", err2) - } - + logrus.Debugf("lcowdriver: startServiceVmIfNotRunning: (%s) hot-adding scratch %s", context, scratchTargetFile) + if err := svm.hotAddVHDsAtStart(hcsshim.MappedVirtualDisk{ + HostPath: scratchTargetFile, + ContainerPath: toolsScratchPath, + CreateInUtilityVM: true, + }); err != nil { + logrus.Debugf("%s: failed to hot-add scratch %s: %s", title, scratchTargetFile, err) return nil, fmt.Errorf("failed to hot-add %s failed: %s", scratchTargetFile, err) } - logrus.Debugf("%s (%s): releasing serviceVM", title, context) - svm.Unlock() + svm.scratchAttached = true } logrus.Debugf("lcowdriver: startServiceVmIfNotRunning: (%s) success", context) return svm, nil } -// getServiceVM returns the appropriate service utility VM instance, optionally -// deleting it from the map (but not the global one) -func (d *Driver) getServiceVM(id string, deleteFromMap bool) (*serviceVMItem, error) { - logrus.Debugf("lcowdriver: getservicevm:locking serviceVmsMutex") - d.serviceVmsMutex.Lock() - defer func() { - logrus.Debugf("lcowdriver: getservicevm:releasing serviceVmsMutex") - d.serviceVmsMutex.Unlock() - }() - if d.globalMode { - id = svmGlobalID - } - if _, ok := d.serviceVms[id]; !ok { - return nil, fmt.Errorf("getservicevm for %s failed as not found", id) - } - svm := d.serviceVms[id] - if deleteFromMap && id != svmGlobalID { - logrus.Debugf("lcowdriver: getservicevm: removing %s from map", id) - delete(d.serviceVms, id) - } - return svm, nil -} - -// terminateServiceVM terminates a service utility VM if its running, but does nothing -// when in global mode as it's lifetime is limited to that of the daemon. -func (d *Driver) terminateServiceVM(id, context string, force bool) error { - +// terminateServiceVM terminates a service utility VM if its running if it's, +// not being used by any goroutine, but does nothing when in global mode as it's +// lifetime is limited to that of the daemon. If the force flag is set, then +// the VM will be killed regardless of the ref count or if it's global. +func (d *Driver) terminateServiceVM(id, context string, force bool) (err error) { // We don't do anything in safe mode unless the force flag has been passed, which // is only the case for cleanup at driver termination. - if d.globalMode { - if !force { - logrus.Debugf("lcowdriver: terminateservicevm: %s (%s) - doing nothing as in global mode", id, context) - return nil - } - id = svmGlobalID + if d.globalMode && !force { + logrus.Debugf("lcowdriver: terminateservicevm: %s (%s) - doing nothing as in global mode", id, context) + return nil } - // Get the service VM and delete it from the map - svm, err := d.getServiceVM(id, true) - if err != nil { - return err + id = d.getVMID(id) + + var svm *serviceVM + var lastRef bool + if !force { + // In the not force case, we ref count + svm, lastRef, err = d.serviceVms.decrementRefCount(id) + } else { + // In the force case, we ignore the ref count and just set it to 0 + svm, err = d.serviceVms.setRefCountZero(id) + lastRef = true + } + + if err == errVMUnknown { + return nil + } else if err == errVMisTerminating { + return svm.getStopError() + } else if !lastRef { + return nil } // We run the deletion of the scratch as a deferred function to at least attempt @@ -459,29 +394,67 @@ func (d *Driver) terminateServiceVM(id, context string, force bool) error { if svm.scratchAttached { scratchTargetFile := filepath.Join(d.dataRoot, scratchDirectory, fmt.Sprintf("%s.vhdx", id)) logrus.Debugf("lcowdriver: terminateservicevm: %s (%s) - deleting scratch %s", id, context, scratchTargetFile) - if err := os.Remove(scratchTargetFile); err != nil { - logrus.Warnf("failed to remove scratch file %s (%s): %s", scratchTargetFile, context, err) + if errRemove := os.Remove(scratchTargetFile); errRemove != nil { + logrus.Warnf("failed to remove scratch file %s (%s): %s", scratchTargetFile, context, errRemove) + err = errRemove } } + + // This function shouldn't actually return error unless there is a bug + if errDelete := d.serviceVms.deleteID(id); errDelete != nil { + logrus.Warnf("failed to service vm from svm map %s (%s): %s", id, context, errDelete) + } + + // Signal that this VM has stopped + svm.signalStopFinished(err) }() - // Nothing to do if it's not running - if svm.config.Uvm != nil { - logrus.Debugf("lcowdriver: terminateservicevm: %s (%s) - calling terminate", id, context) - if err := svm.config.Uvm.Terminate(); err != nil { - return fmt.Errorf("failed to terminate utility VM (%s): %s", context, err) - } + // Now it's possible that the serivce VM failed to start and now we are trying to termiante it. + // In this case, we will relay the error to the goroutines waiting for this vm to stop. + if err := svm.getStartError(); err != nil { + logrus.Debugf("lcowdriver: terminateservicevm: %s had failed to start up: %s", id, err) + return err + } - logrus.Debugf("lcowdriver: terminateservicevm: %s (%s) - waiting for utility VM to terminate", id, context) - if err := svm.config.Uvm.WaitTimeout(time.Duration(svm.config.UvmTimeoutSeconds) * time.Second); err != nil { - return fmt.Errorf("failed waiting for utility VM to terminate (%s): %s", context, err) - } + if err := waitTerminate(svm, fmt.Sprintf("terminateservicevm: %s (%s)", id, context)); err != nil { + return err } logrus.Debugf("lcowdriver: terminateservicevm: %s (%s) - success", id, context) return nil } +func waitTerminate(svm *serviceVM, context string) error { + if svm.config == nil { + return fmt.Errorf("lcowdriver: waitTermiante: Nil utility VM. %s", context) + } + + logrus.Debugf("lcowdriver: waitTerminate: Calling terminate: %s", context) + if err := svm.config.Uvm.Terminate(); err != nil { + // We might get operation still pending from the HCS. In that case, we shouldn't return + // an error since we call wait right after. + underlyingError := err + if conterr, ok := err.(*hcsshim.ContainerError); ok { + underlyingError = conterr.Err + } + + if syscallErr, ok := underlyingError.(syscall.Errno); ok { + underlyingError = syscallErr + } + + if underlyingError != errOperationPending { + return fmt.Errorf("failed to terminate utility VM (%s): %s", context, err) + } + logrus.Debugf("lcowdriver: waitTerminate: uvm.Terminate() returned operation pending (%s)", context) + } + + logrus.Debugf("lcowdriver: waitTerminate: (%s) - waiting for utility VM to terminate", context) + if err := svm.config.Uvm.WaitTimeout(time.Duration(svm.config.UvmTimeoutSeconds) * time.Second); err != nil { + return fmt.Errorf("failed waiting for utility VM to terminate (%s): %s", context, err) + } + return nil +} + // String returns the string representation of a driver. This should match // the name the graph driver has been registered with. func (d *Driver) String() string { @@ -571,25 +544,18 @@ func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts }() } - // Synchronise the operation in the service VM. - logrus.Debugf("%s: locking svm for sandbox creation", title) - svm.Lock() - defer func() { - logrus.Debugf("%s: releasing svm for sandbox creation", title) - svm.Unlock() - }() - // Make sure we don't write to our local cached copy if this is for a non-default size request. targetCacheFile := d.cachedSandboxFile if sandboxSize != client.DefaultVhdxSizeGB { targetCacheFile = "" } - // Actually do the creation. - if err := svm.config.CreateExt4Vhdx(filepath.Join(d.dir(id), sandboxFilename), uint32(sandboxSize), targetCacheFile); err != nil { + // Create the ext4 vhdx + logrus.Debugf("%s: creating sandbox ext4 vhdx", title) + if err := svm.createExt4VHDX(filepath.Join(d.dir(id), sandboxFilename), uint32(sandboxSize), targetCacheFile); err != nil { + logrus.Debugf("%s: failed to create sandbox vhdx for %s: %s", title, id, err) return err } - return nil } @@ -638,6 +604,21 @@ func (d *Driver) Remove(id string) error { layerPath := d.dir(id) logrus.Debugf("lcowdriver: remove: id %s: layerPath %s", id, layerPath) + + // Unmount all the layers + err := d.Put(id) + if err != nil { + logrus.Debugf("lcowdriver: remove id %s: failed to unmount: %s", id, err) + return err + } + + // for non-global case just kill the vm + if !d.globalMode { + if err := d.terminateServiceVM(id, fmt.Sprintf("Remove %s", id), true); err != nil { + return err + } + } + if err := os.Rename(layerPath, tmpLayerPath); err != nil && !os.IsNotExist(err) { return err } @@ -659,43 +640,24 @@ func (d *Driver) Remove(id string) error { // For optimisation, we don't actually mount the filesystem (which in our // case means [hot-]adding it to a service VM. But we track that and defer // the actual adding to the point we need to access it. -func (d *Driver) Get(id, mountLabel string) (string, error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { title := fmt.Sprintf("lcowdriver: get: %s", id) logrus.Debugf(title) - // Work out what we are working on - ld, err := getLayerDetails(d.dir(id)) + // Generate the mounts needed for the defered operation. + disks, err := d.getAllMounts(id) if err != nil { - logrus.Debugf("%s failed to get layer details from %s: %s", title, d.dir(id), err) - return "", fmt.Errorf("%s failed to open layer or sandbox VHD to open in %s: %s", title, d.dir(id), err) + logrus.Debugf("%s failed to get all layer details for %s: %s", title, d.dir(id), err) + return nil, fmt.Errorf("%s failed to get layer details for %s: %s", title, d.dir(id), err) } - logrus.Debugf("%s %s, size %d, isSandbox %t", title, ld.filename, ld.size, ld.isSandbox) - // Add item to cache, or update existing item, but ensure we have the - // lock while updating items. - logrus.Debugf("%s: locking cacheMutex", title) - d.cacheMutex.Lock() - var ci *cacheItem - if item, ok := d.cache[id]; !ok { - // The item is not currently in the cache. - ci = &cacheItem{ - refCount: 1, - isSandbox: ld.isSandbox, - hostPath: ld.filename, - uvmPath: fmt.Sprintf("/mnt/%s", id), - isMounted: false, // we defer this as an optimisation - } - d.cache[id] = ci - logrus.Debugf("%s: added cache item %+v", title, ci) - } else { - // Increment the reference counter in the cache. - item.incrementRefCount() - } - logrus.Debugf("%s: releasing cacheMutex", title) - d.cacheMutex.Unlock() - - logrus.Debugf("%s %s success. %s: %+v: size %d", title, id, d.dir(id), ci, ld.size) - return d.dir(id), nil + logrus.Debugf("%s: got layer mounts: %+v", title, disks) + return &lcowfs{ + root: unionMountName(disks), + d: d, + mappedDisks: disks, + vmID: d.getVMID(id), + }, nil } // Put does the reverse of get. If there are no more references to @@ -703,56 +665,45 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { func (d *Driver) Put(id string) error { title := fmt.Sprintf("lcowdriver: put: %s", id) - logrus.Debugf("%s: locking cacheMutex", title) - d.cacheMutex.Lock() - item, ok := d.cache[id] - if !ok { - logrus.Debugf("%s: releasing cacheMutex on error path", title) - d.cacheMutex.Unlock() - return fmt.Errorf("%s possible ref-count error, or invalid id was passed to the graphdriver. Cannot handle id %s as it's not in the cache", title, id) - } - - // Decrement the ref-count, and nothing more to do if still in use. - if item.decrementRefCount() > 0 { - logrus.Debugf("%s: releasing cacheMutex. Cache item is still in use", title) - d.cacheMutex.Unlock() + // Get the service VM that we need to remove from + svm, err := d.serviceVms.get(d.getVMID(id)) + if err == errVMUnknown { return nil + } else if err == errVMisTerminating { + return svm.getStopError() } - // Remove from the cache map. - delete(d.cache, id) - logrus.Debugf("%s: releasing cacheMutex. Ref count on cache item has dropped to zero, removed from cache", title) - d.cacheMutex.Unlock() + // Generate the mounts that Get() might have mounted + disks, err := d.getAllMounts(id) + if err != nil { + logrus.Debugf("%s failed to get all layer details for %s: %s", title, d.dir(id), err) + return fmt.Errorf("%s failed to get layer details for %s: %s", title, d.dir(id), err) + } - // If we have done a mount and we are in global mode, then remove it. We don't - // need to remove in safe mode as the service VM is going to be torn down anyway. - if d.globalMode { - logrus.Debugf("%s: locking cache item at zero ref-count", title) - item.Lock() - defer func() { - logrus.Debugf("%s: releasing cache item at zero ref-count", title) - item.Unlock() - }() - if item.isMounted { - svm, err := d.getServiceVM(id, false) - if err != nil { - return err - } + // Now, we want to perform the unmounts, hot-remove and stop the service vm. + // We want to go though all the steps even if we have an error to clean up properly + err = svm.deleteUnionMount(unionMountName(disks), disks...) + if err != nil { + logrus.Debugf("%s failed to delete union mount %s: %s", title, id, err) + } - logrus.Debugf("%s: Hot-Removing %s. Locking svm", title, item.hostPath) - svm.Lock() - if err := svm.config.HotRemoveVhd(item.hostPath); err != nil { - logrus.Debugf("%s: releasing svm on error path", title) - svm.Unlock() - return fmt.Errorf("%s failed to hot-remove %s from global service utility VM: %s", title, item.hostPath, err) - } - logrus.Debugf("%s: releasing svm", title) - svm.Unlock() + err1 := svm.hotRemoveVHDs(disks...) + if err1 != nil { + logrus.Debugf("%s failed to hot remove vhds %s: %s", title, id, err) + if err == nil { + err = err1 } } - logrus.Debugf("%s %s: refCount 0. %s (%s) completed successfully", title, id, item.hostPath, item.uvmPath) - return nil + err1 = d.terminateServiceVM(id, fmt.Sprintf("Put %s", id), false) + if err1 != nil { + logrus.Debugf("%s failed to terminate service vm %s: %s", title, id, err1) + if err == nil { + err = err1 + } + } + logrus.Debugf("Put succeeded on id %s", id) + return err } // Cleanup ensures the information the driver stores is properly removed. @@ -761,15 +712,6 @@ func (d *Driver) Put(id string) error { func (d *Driver) Cleanup() error { title := "lcowdriver: cleanup" - d.cacheMutex.Lock() - for k, v := range d.cache { - logrus.Debugf("%s cache item: %s: %+v", title, k, v) - if v.refCount > 0 { - logrus.Warnf("%s leaked %s: %+v", title, k, v) - } - } - d.cacheMutex.Unlock() - items, err := ioutil.ReadDir(d.dataRoot) if err != nil { if os.IsNotExist(err) { @@ -794,8 +736,8 @@ func (d *Driver) Cleanup() error { // Cleanup any service VMs we have running, along with their scratch spaces. // We don't take the lock for this as it's taken in terminateServiceVm. - for k, v := range d.serviceVms { - logrus.Debugf("%s svm: %s: %+v", title, k, v) + for k, v := range d.serviceVms.svms { + logrus.Debugf("%s svm entry: %s: %+v", title, k, v) d.terminateServiceVM(k, "cleanup", true) } @@ -812,65 +754,41 @@ func (d *Driver) Cleanup() error { func (d *Driver) Diff(id, parent string) (io.ReadCloser, error) { title := fmt.Sprintf("lcowdriver: diff: %s", id) - logrus.Debugf("%s: locking cacheMutex", title) - d.cacheMutex.Lock() - if _, ok := d.cache[id]; !ok { - logrus.Debugf("%s: releasing cacheMutex on error path", title) - d.cacheMutex.Unlock() - return nil, fmt.Errorf("%s fail as %s is not in the cache", title, id) - } - ci := d.cache[id] - logrus.Debugf("%s: releasing cacheMutex", title) - d.cacheMutex.Unlock() - - // Stat to get size - logrus.Debugf("%s: locking cacheItem", title) - ci.Lock() - fileInfo, err := os.Stat(ci.hostPath) + // Get VHDX info + ld, err := getLayerDetails(d.dir(id)) if err != nil { - logrus.Debugf("%s: releasing cacheItem on error path", title) - ci.Unlock() - return nil, fmt.Errorf("%s failed to stat %s: %s", title, ci.hostPath, err) + logrus.Debugf("%s: failed to get vhdx information of %s: %s", title, d.dir(id), err) + return nil, err } - logrus.Debugf("%s: releasing cacheItem", title) - ci.Unlock() // Start the SVM with a mapped virtual disk. Note that if the SVM is // already running and we are in global mode, this will be // hot-added. - mvd := &hcsshim.MappedVirtualDisk{ - HostPath: ci.hostPath, - ContainerPath: ci.uvmPath, + mvd := hcsshim.MappedVirtualDisk{ + HostPath: ld.filename, + ContainerPath: hostToGuest(ld.filename), CreateInUtilityVM: true, ReadOnly: true, } logrus.Debugf("%s: starting service VM", title) - svm, err := d.startServiceVMIfNotRunning(id, mvd, fmt.Sprintf("diff %s", id)) + svm, err := d.startServiceVMIfNotRunning(id, []hcsshim.MappedVirtualDisk{mvd}, fmt.Sprintf("diff %s", id)) if err != nil { return nil, err } - // Set `isMounted` for the cache item. Note that we re-scan the cache - // at this point as it's possible the cacheItem changed during the long- - // running operation above when we weren't holding the cacheMutex lock. - logrus.Debugf("%s: locking cacheMutex for updating isMounted", title) - d.cacheMutex.Lock() - if _, ok := d.cache[id]; !ok { - logrus.Debugf("%s: releasing cacheMutex on error path of isMounted", title) - d.cacheMutex.Unlock() + logrus.Debugf("lcowdriver: diff: waiting for svm to finish booting") + err = svm.getStartError() + if err != nil { d.terminateServiceVM(id, fmt.Sprintf("diff %s", id), false) - return nil, fmt.Errorf("%s fail as %s is not in the cache when updating isMounted", title, id) + return nil, fmt.Errorf("lcowdriver: diff: svm failed to boot: %s", err) } - ci = d.cache[id] - ci.setIsMounted() - logrus.Debugf("%s: releasing cacheMutex for updating isMounted", title) - d.cacheMutex.Unlock() // Obtain the tar stream for it - logrus.Debugf("%s %s, size %d, isSandbox %t", title, ci.hostPath, fileInfo.Size(), ci.isSandbox) - tarReadCloser, err := svm.config.VhdToTar(ci.hostPath, ci.uvmPath, ci.isSandbox, fileInfo.Size()) + logrus.Debugf("%s: %s %s, size %d, ReadOnly %t", title, ld.filename, mvd.ContainerPath, ld.size, ld.isSandbox) + tarReadCloser, err := svm.config.VhdToTar(mvd.HostPath, mvd.ContainerPath, ld.isSandbox, ld.size) if err != nil { + svm.hotRemoveVHDs(mvd) d.terminateServiceVM(id, fmt.Sprintf("diff %s", id), false) return nil, fmt.Errorf("%s failed to export layer to tar stream for id: %s, parent: %s : %s", title, id, parent, err) } @@ -878,14 +796,12 @@ func (d *Driver) Diff(id, parent string) (io.ReadCloser, error) { logrus.Debugf("%s id %s parent %s completed successfully", title, id, parent) // In safe/non-global mode, we can't tear down the service VM until things have been read. - if !d.globalMode { - return ioutils.NewReadCloserWrapper(tarReadCloser, func() error { - tarReadCloser.Close() - d.terminateServiceVM(id, fmt.Sprintf("diff %s", id), false) - return nil - }), nil - } - return tarReadCloser, nil + return ioutils.NewReadCloserWrapper(tarReadCloser, func() error { + tarReadCloser.Close() + svm.hotRemoveVHDs(mvd) + d.terminateServiceVM(id, fmt.Sprintf("diff %s", id), false) + return nil + }), nil } // ApplyDiff extracts the changeset from the given diff into the @@ -902,6 +818,12 @@ func (d *Driver) ApplyDiff(id, parent string, diff io.Reader) (int64, error) { } defer d.terminateServiceVM(id, fmt.Sprintf("applydiff %s", id), false) + logrus.Debugf("lcowdriver: applydiff: waiting for svm to finish booting") + err = svm.getStartError() + if err != nil { + return 0, fmt.Errorf("lcowdriver: applydiff: svm failed to boot: %s", err) + } + // TODO @jhowardmsft - the retries are temporary to overcome platform reliablity issues. // Obviously this will be removed as platform bugs are fixed. retries := 0 @@ -944,6 +866,11 @@ func (d *Driver) GetMetadata(id string) (map[string]string, error) { return m, nil } +// GetLayerPath gets the layer path on host (path to VHD/VHDX) +func (d *Driver) GetLayerPath(id string) (string, error) { + return d.dir(id), nil +} + // dir returns the absolute path to the layer. func (d *Driver) dir(id string) string { return filepath.Join(d.dataRoot, filepath.Base(id)) @@ -1006,3 +933,34 @@ func getLayerDetails(folder string) (*layerDetails, error) { return ld, nil } + +func (d *Driver) getAllMounts(id string) ([]hcsshim.MappedVirtualDisk, error) { + layerChain, err := d.getLayerChain(id) + if err != nil { + return nil, err + } + layerChain = append([]string{d.dir(id)}, layerChain...) + + logrus.Debugf("getting all layers: %v", layerChain) + disks := make([]hcsshim.MappedVirtualDisk, len(layerChain), len(layerChain)) + for i := range layerChain { + ld, err := getLayerDetails(layerChain[i]) + if err != nil { + logrus.Debugf("Failed to get LayerVhdDetails from %s: %s", layerChain[i], err) + return nil, err + } + disks[i].HostPath = ld.filename + disks[i].ContainerPath = hostToGuest(ld.filename) + disks[i].CreateInUtilityVM = true + disks[i].ReadOnly = !ld.isSandbox + } + return disks, nil +} + +func hostToGuest(hostpath string) string { + return fmt.Sprintf("/tmp/%s", filepath.Base(filepath.Dir(hostpath))) +} + +func unionMountName(disks []hcsshim.MappedVirtualDisk) string { + return fmt.Sprintf("%s-mount", disks[0].ContainerPath) +} diff --git a/daemon/graphdriver/lcow/lcow_svm.go b/daemon/graphdriver/lcow/lcow_svm.go new file mode 100644 index 0000000000..26f6df4f03 --- /dev/null +++ b/daemon/graphdriver/lcow/lcow_svm.go @@ -0,0 +1,373 @@ +// +build windows + +package lcow + +import ( + "errors" + "fmt" + "io" + "strings" + "sync" + "time" + + "github.com/Microsoft/hcsshim" + "github.com/Microsoft/opengcs/client" + "github.com/sirupsen/logrus" +) + +// Code for all the service VM management for the LCOW graphdriver + +var errVMisTerminating = errors.New("service VM is shutting down") +var errVMUnknown = errors.New("service vm id is unknown") +var errVMStillHasReference = errors.New("Attemping to delete a VM that is still being used") + +// serviceVMMap is the struct representing the id -> service VM mapping. +type serviceVMMap struct { + sync.Mutex + svms map[string]*serviceVMMapItem +} + +// serviceVMMapItem is our internal structure representing an item in our +// map of service VMs we are maintaining. +type serviceVMMapItem struct { + svm *serviceVM // actual service vm object + refCount int // refcount for VM +} + +type serviceVM struct { + sync.Mutex // Serialises operations being performed in this service VM. + scratchAttached bool // Has a scratch been attached? + config *client.Config // Represents the service VM item. + + // Indicates that the vm is started + startStatus chan interface{} + startError error + + // Indicates that the vm is stopped + stopStatus chan interface{} + stopError error + + attachedVHDs map[string]int // Map ref counting all the VHDS we've hot-added/hot-removed. + unionMounts map[string]int // Map ref counting all the union filesystems we mounted. +} + +// add will add an id to the service vm map. There are three cases: +// - entry doesn't exist: +// - add id to map and return a new vm that the caller can manually configure+start +// - entry does exist +// - return vm in map and increment ref count +// - entry does exist but the ref count is 0 +// - return the svm and errVMisTerminating. Caller can call svm.getStopError() to wait for stop +func (svmMap *serviceVMMap) add(id string) (svm *serviceVM, alreadyExists bool, err error) { + svmMap.Lock() + defer svmMap.Unlock() + if svm, ok := svmMap.svms[id]; ok { + if svm.refCount == 0 { + return svm.svm, true, errVMisTerminating + } + svm.refCount++ + return svm.svm, true, nil + } + + // Doesn't exist, so create an empty svm to put into map and return + newSVM := &serviceVM{ + startStatus: make(chan interface{}), + stopStatus: make(chan interface{}), + attachedVHDs: make(map[string]int), + unionMounts: make(map[string]int), + config: &client.Config{}, + } + svmMap.svms[id] = &serviceVMMapItem{ + svm: newSVM, + refCount: 1, + } + return newSVM, false, nil +} + +// get will get the service vm from the map. There are three cases: +// - entry doesn't exist: +// - return errVMUnknown +// - entry does exist +// - return vm with no error +// - entry does exist but the ref count is 0 +// - return the svm and errVMisTerminating. Caller can call svm.getStopError() to wait for stop +func (svmMap *serviceVMMap) get(id string) (*serviceVM, error) { + svmMap.Lock() + defer svmMap.Unlock() + svm, ok := svmMap.svms[id] + if !ok { + return nil, errVMUnknown + } + if svm.refCount == 0 { + return svm.svm, errVMisTerminating + } + return svm.svm, nil +} + +// decrementRefCount decrements the ref count of the given ID from the map. There are four cases: +// - entry doesn't exist: +// - return errVMUnknown +// - entry does exist but the ref count is 0 +// - return the svm and errVMisTerminating. Caller can call svm.getStopError() to wait for stop +// - entry does exist but ref count is 1 +// - return vm and set lastRef to true. The caller can then stop the vm, delete the id from this map +// - and execute svm.signalStopFinished to signal the threads that the svm has been terminated. +// - entry does exist and ref count > 1 +// - just reduce ref count and return svm +func (svmMap *serviceVMMap) decrementRefCount(id string) (_ *serviceVM, lastRef bool, _ error) { + svmMap.Lock() + defer svmMap.Unlock() + + svm, ok := svmMap.svms[id] + if !ok { + return nil, false, errVMUnknown + } + if svm.refCount == 0 { + return svm.svm, false, errVMisTerminating + } + svm.refCount-- + return svm.svm, svm.refCount == 0, nil +} + +// setRefCountZero works the same way as decrementRefCount, but sets ref count to 0 instead of decrementing it. +func (svmMap *serviceVMMap) setRefCountZero(id string) (*serviceVM, error) { + svmMap.Lock() + defer svmMap.Unlock() + + svm, ok := svmMap.svms[id] + if !ok { + return nil, errVMUnknown + } + if svm.refCount == 0 { + return svm.svm, errVMisTerminating + } + svm.refCount = 0 + return svm.svm, nil +} + +// deleteID deletes the given ID from the map. If the refcount is not 0 or the +// VM does not exist, then this function returns an error. +func (svmMap *serviceVMMap) deleteID(id string) error { + svmMap.Lock() + defer svmMap.Unlock() + svm, ok := svmMap.svms[id] + if !ok { + return errVMUnknown + } + if svm.refCount != 0 { + return errVMStillHasReference + } + delete(svmMap.svms, id) + return nil +} + +func (svm *serviceVM) signalStartFinished(err error) { + svm.Lock() + svm.startError = err + svm.Unlock() + close(svm.startStatus) +} + +func (svm *serviceVM) getStartError() error { + <-svm.startStatus + svm.Lock() + defer svm.Unlock() + return svm.startError +} + +func (svm *serviceVM) signalStopFinished(err error) { + svm.Lock() + svm.stopError = err + svm.Unlock() + close(svm.stopStatus) +} + +func (svm *serviceVM) getStopError() error { + <-svm.stopStatus + svm.Lock() + defer svm.Unlock() + return svm.stopError +} + +// hotAddVHDs waits for the service vm to start and then attaches the vhds. +func (svm *serviceVM) hotAddVHDs(mvds ...hcsshim.MappedVirtualDisk) error { + if err := svm.getStartError(); err != nil { + return err + } + return svm.hotAddVHDsAtStart(mvds...) +} + +// hotAddVHDsAtStart works the same way as hotAddVHDs but does not wait for the VM to start. +func (svm *serviceVM) hotAddVHDsAtStart(mvds ...hcsshim.MappedVirtualDisk) error { + svm.Lock() + defer svm.Unlock() + for i, mvd := range mvds { + if _, ok := svm.attachedVHDs[mvd.HostPath]; ok { + svm.attachedVHDs[mvd.HostPath]++ + continue + } + + if err := svm.config.HotAddVhd(mvd.HostPath, mvd.ContainerPath, mvd.ReadOnly, !mvd.AttachOnly); err != nil { + svm.hotRemoveVHDsAtStart(mvds[:i]...) + return err + } + svm.attachedVHDs[mvd.HostPath] = 1 + } + return nil +} + +// hotRemoveVHDs waits for the service vm to start and then removes the vhds. +func (svm *serviceVM) hotRemoveVHDs(mvds ...hcsshim.MappedVirtualDisk) error { + if err := svm.getStartError(); err != nil { + return err + } + return svm.hotRemoveVHDsAtStart(mvds...) +} + +// hotRemoveVHDsAtStart works the same way as hotRemoveVHDs but does not wait for the VM to start. +func (svm *serviceVM) hotRemoveVHDsAtStart(mvds ...hcsshim.MappedVirtualDisk) error { + svm.Lock() + defer svm.Unlock() + var retErr error + for _, mvd := range mvds { + if _, ok := svm.attachedVHDs[mvd.HostPath]; !ok { + // We continue instead of returning an error if we try to hot remove a non-existent VHD. + // This is because one of the callers of the function is graphdriver.Put(). Since graphdriver.Get() + // defers the VM start to the first operation, it's possible that nothing have been hot-added + // when Put() is called. To avoid Put returning an error in that case, we simply continue if we + // don't find the vhd attached. + continue + } + + if svm.attachedVHDs[mvd.HostPath] > 1 { + svm.attachedVHDs[mvd.HostPath]-- + continue + } + + // last VHD, so remove from VM and map + if err := svm.config.HotRemoveVhd(mvd.HostPath); err == nil { + delete(svm.attachedVHDs, mvd.HostPath) + } else { + // Take note of the error, but still continue to remove the other VHDs + logrus.Warnf("Failed to hot remove %s: %s", mvd.HostPath, err) + if retErr == nil { + retErr = err + } + } + } + return retErr +} + +func (svm *serviceVM) createExt4VHDX(destFile string, sizeGB uint32, cacheFile string) error { + if err := svm.getStartError(); err != nil { + return err + } + + svm.Lock() + defer svm.Unlock() + return svm.config.CreateExt4Vhdx(destFile, sizeGB, cacheFile) +} + +func (svm *serviceVM) createUnionMount(mountName string, mvds ...hcsshim.MappedVirtualDisk) (err error) { + if len(mvds) == 0 { + return fmt.Errorf("createUnionMount: error must have at least 1 layer") + } + + if err = svm.getStartError(); err != nil { + return err + } + + svm.Lock() + defer svm.Unlock() + if _, ok := svm.unionMounts[mountName]; ok { + svm.unionMounts[mountName]++ + return nil + } + + var lowerLayers []string + if mvds[0].ReadOnly { + lowerLayers = append(lowerLayers, mvds[0].ContainerPath) + } + + for i := 1; i < len(mvds); i++ { + lowerLayers = append(lowerLayers, mvds[i].ContainerPath) + } + + logrus.Debugf("Doing the overlay mount with union directory=%s", mountName) + if err = svm.runProcess(fmt.Sprintf("mkdir -p %s", mountName), nil, nil, nil); err != nil { + return err + } + + var cmd string + if mvds[0].ReadOnly { + // Readonly overlay + cmd = fmt.Sprintf("mount -t overlay overlay -olowerdir=%s %s", + strings.Join(lowerLayers, ","), + mountName) + } else { + upper := fmt.Sprintf("%s/upper", mvds[0].ContainerPath) + work := fmt.Sprintf("%s/work", mvds[0].ContainerPath) + + if err = svm.runProcess(fmt.Sprintf("mkdir -p %s %s", upper, work), nil, nil, nil); err != nil { + return err + } + + cmd = fmt.Sprintf("mount -t overlay overlay -olowerdir=%s,upperdir=%s,workdir=%s %s", + strings.Join(lowerLayers, ":"), + upper, + work, + mountName) + } + + logrus.Debugf("createUnionMount: Executing mount=%s", cmd) + if err = svm.runProcess(cmd, nil, nil, nil); err != nil { + return err + } + + svm.unionMounts[mountName] = 1 + return nil +} + +func (svm *serviceVM) deleteUnionMount(mountName string, disks ...hcsshim.MappedVirtualDisk) error { + if err := svm.getStartError(); err != nil { + return err + } + + svm.Lock() + defer svm.Unlock() + if _, ok := svm.unionMounts[mountName]; !ok { + return nil + } + + if svm.unionMounts[mountName] > 1 { + svm.unionMounts[mountName]-- + return nil + } + + logrus.Debugf("Removing union mount %s", mountName) + if err := svm.runProcess(fmt.Sprintf("umount %s", mountName), nil, nil, nil); err != nil { + return err + } + + delete(svm.unionMounts, mountName) + return nil +} + +func (svm *serviceVM) runProcess(command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + process, err := svm.config.RunProcess(command, stdin, stdout, stderr) + if err != nil { + return err + } + defer process.Close() + + process.WaitTimeout(time.Duration(int(time.Second) * svm.config.UvmTimeoutSeconds)) + exitCode, err := process.ExitCode() + if err != nil { + return err + } + + if exitCode != 0 { + return fmt.Errorf("svm.runProcess: command %s failed with exit code %d", command, exitCode) + } + return nil +} diff --git a/daemon/graphdriver/lcow/remotefs.go b/daemon/graphdriver/lcow/remotefs.go new file mode 100644 index 0000000000..148e3c0a2d --- /dev/null +++ b/daemon/graphdriver/lcow/remotefs.go @@ -0,0 +1,139 @@ +// +build windows + +package lcow + +import ( + "bytes" + "fmt" + "io" + "runtime" + "strings" + "sync" + + "github.com/Microsoft/hcsshim" + "github.com/Microsoft/opengcs/service/gcsutils/remotefs" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" + "github.com/sirupsen/logrus" +) + +type lcowfs struct { + root string + d *Driver + mappedDisks []hcsshim.MappedVirtualDisk + vmID string + currentSVM *serviceVM + sync.Mutex +} + +var _ containerfs.ContainerFS = &lcowfs{} + +// ErrNotSupported is an error for unsupported operations in the remotefs +var ErrNotSupported = fmt.Errorf("not supported") + +// Functions to implement the ContainerFS interface +func (l *lcowfs) Path() string { + return l.root +} + +func (l *lcowfs) ResolveScopedPath(path string, rawPath bool) (string, error) { + logrus.Debugf("remotefs.resolvescopedpath inputs: %s %s ", path, l.root) + + arg1 := l.Join(l.root, path) + if !rawPath { + // The l.Join("/", path) will make path an absolute path and then clean it + // so if path = ../../X, it will become /X. + arg1 = l.Join(l.root, l.Join("/", path)) + } + arg2 := l.root + + output := &bytes.Buffer{} + if err := l.runRemoteFSProcess(nil, output, remotefs.ResolvePathCmd, arg1, arg2); err != nil { + return "", err + } + + logrus.Debugf("remotefs.resolvescopedpath success. Output: %s\n", output.String()) + return output.String(), nil +} + +func (l *lcowfs) OS() string { + return "linux" +} + +func (l *lcowfs) Architecture() string { + return runtime.GOARCH +} + +// Other functions that are used by docker like the daemon Archiver/Extractor +func (l *lcowfs) ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error { + logrus.Debugf("remotefs.ExtractArchve inputs: %s %+v", dst, opts) + + tarBuf := &bytes.Buffer{} + if err := remotefs.WriteTarOptions(tarBuf, opts); err != nil { + return fmt.Errorf("failed to marshall tar opts: %s", err) + } + + input := io.MultiReader(tarBuf, src) + if err := l.runRemoteFSProcess(input, nil, remotefs.ExtractArchiveCmd, dst); err != nil { + return fmt.Errorf("failed to extract archive to %s: %s", dst, err) + } + return nil +} + +func (l *lcowfs) ArchivePath(src string, opts *archive.TarOptions) (io.ReadCloser, error) { + logrus.Debugf("remotefs.ArchivePath: %s %+v", src, opts) + + tarBuf := &bytes.Buffer{} + if err := remotefs.WriteTarOptions(tarBuf, opts); err != nil { + return nil, fmt.Errorf("failed to marshall tar opts: %s", err) + } + + r, w := io.Pipe() + go func() { + defer w.Close() + if err := l.runRemoteFSProcess(tarBuf, w, remotefs.ArchivePathCmd, src); err != nil { + logrus.Debugf("REMOTEFS: Failed to extract archive: %s %+v %s", src, opts, err) + } + }() + return r, nil +} + +// Helper functions +func (l *lcowfs) startVM() error { + l.Lock() + defer l.Unlock() + if l.currentSVM != nil { + return nil + } + + svm, err := l.d.startServiceVMIfNotRunning(l.vmID, l.mappedDisks, fmt.Sprintf("lcowfs.startVM")) + if err != nil { + return err + } + + if err = svm.createUnionMount(l.root, l.mappedDisks...); err != nil { + return err + } + l.currentSVM = svm + return nil +} + +func (l *lcowfs) runRemoteFSProcess(stdin io.Reader, stdout io.Writer, args ...string) error { + if err := l.startVM(); err != nil { + return err + } + + // Append remotefs prefix and setup as a command line string + cmd := fmt.Sprintf("%s %s", remotefs.RemotefsCmd, strings.Join(args, " ")) + stderr := &bytes.Buffer{} + if err := l.currentSVM.runProcess(cmd, stdin, stdout, stderr); err != nil { + return err + } + + eerr, err := remotefs.ReadError(stderr) + if eerr != nil { + // Process returned an error so return that. + return remotefs.ExportedToError(eerr) + } + return err +} diff --git a/daemon/graphdriver/lcow/remotefs_file.go b/daemon/graphdriver/lcow/remotefs_file.go new file mode 100644 index 0000000000..c13431973c --- /dev/null +++ b/daemon/graphdriver/lcow/remotefs_file.go @@ -0,0 +1,211 @@ +// +build windows + +package lcow + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "strconv" + + "github.com/Microsoft/hcsshim" + "github.com/Microsoft/opengcs/service/gcsutils/remotefs" + "github.com/containerd/continuity/driver" +) + +type lcowfile struct { + process hcsshim.Process + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + fs *lcowfs + guestPath string +} + +func (l *lcowfs) Open(path string) (driver.File, error) { + return l.OpenFile(path, os.O_RDONLY, 0) +} + +func (l *lcowfs) OpenFile(path string, flag int, perm os.FileMode) (_ driver.File, err error) { + flagStr := strconv.FormatInt(int64(flag), 10) + permStr := strconv.FormatUint(uint64(perm), 8) + + commandLine := fmt.Sprintf("%s %s %s %s", remotefs.RemotefsCmd, remotefs.OpenFileCmd, flagStr, permStr) + env := make(map[string]string) + env["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:" + processConfig := &hcsshim.ProcessConfig{ + EmulateConsole: false, + CreateStdInPipe: true, + CreateStdOutPipe: true, + CreateStdErrPipe: true, + CreateInUtilityVm: true, + WorkingDirectory: "/bin", + Environment: env, + CommandLine: commandLine, + } + + process, err := l.currentSVM.config.Uvm.CreateProcess(processConfig) + if err != nil { + return nil, fmt.Errorf("failed to open file %s: %s", path, err) + } + + stdin, stdout, stderr, err := process.Stdio() + if err != nil { + process.Kill() + process.Close() + return nil, fmt.Errorf("failed to open file pipes %s: %s", path, err) + } + + lf := &lcowfile{ + process: process, + stdin: stdin, + stdout: stdout, + stderr: stderr, + fs: l, + guestPath: path, + } + + if _, err := lf.getResponse(); err != nil { + return nil, fmt.Errorf("failed to open file %s: %s", path, err) + } + return lf, nil +} + +func (l *lcowfile) Read(b []byte) (int, error) { + hdr := &remotefs.FileHeader{ + Cmd: remotefs.Read, + Size: uint64(len(b)), + } + + if err := remotefs.WriteFileHeader(l.stdin, hdr, nil); err != nil { + return 0, err + } + + buf, err := l.getResponse() + if err != nil { + return 0, nil + } + + n := copy(b, buf) + return n, nil +} + +func (l *lcowfile) Write(b []byte) (int, error) { + hdr := &remotefs.FileHeader{ + Cmd: remotefs.Write, + Size: uint64(len(b)), + } + + if err := remotefs.WriteFileHeader(l.stdin, hdr, b); err != nil { + return 0, err + } + + _, err := l.getResponse() + if err != nil { + return 0, nil + } + + return len(b), nil +} + +func (l *lcowfile) Seek(offset int64, whence int) (int64, error) { + seekHdr := &remotefs.SeekHeader{ + Offset: offset, + Whence: int32(whence), + } + + buf := &bytes.Buffer{} + if err := binary.Write(buf, binary.BigEndian, seekHdr); err != nil { + return 0, err + } + + hdr := &remotefs.FileHeader{ + Cmd: remotefs.Write, + Size: uint64(buf.Len()), + } + if err := remotefs.WriteFileHeader(l.stdin, hdr, buf.Bytes()); err != nil { + return 0, err + } + + resBuf, err := l.getResponse() + if err != nil { + return 0, err + } + + var res int64 + if err := binary.Read(bytes.NewBuffer(resBuf), binary.BigEndian, &res); err != nil { + return 0, err + } + return res, nil +} + +func (l *lcowfile) Close() error { + hdr := &remotefs.FileHeader{ + Cmd: remotefs.Close, + Size: 0, + } + + if err := remotefs.WriteFileHeader(l.stdin, hdr, nil); err != nil { + return err + } + + _, err := l.getResponse() + return err +} + +func (l *lcowfile) Readdir(n int) ([]os.FileInfo, error) { + nStr := strconv.FormatInt(int64(n), 10) + + // Unlike the other File functions, this one can just be run without maintaining state, + // so just do the normal runRemoteFSProcess way. + buf := &bytes.Buffer{} + if err := l.fs.runRemoteFSProcess(nil, buf, remotefs.ReadDirCmd, l.guestPath, nStr); err != nil { + return nil, err + } + + var info []remotefs.FileInfo + if err := json.Unmarshal(buf.Bytes(), &info); err != nil { + return nil, nil + } + + osInfo := make([]os.FileInfo, len(info)) + for i := range info { + osInfo[i] = &info[i] + } + return osInfo, nil +} + +func (l *lcowfile) getResponse() ([]byte, error) { + hdr, err := remotefs.ReadFileHeader(l.stdout) + if err != nil { + return nil, err + } + + if hdr.Cmd != remotefs.CmdOK { + // Something went wrong during the openfile in the server. + // Parse stderr and return that as an error + eerr, err := remotefs.ReadError(l.stderr) + if eerr != nil { + return nil, remotefs.ExportedToError(eerr) + } + + // Maybe the parsing went wrong? + if err != nil { + return nil, err + } + + // At this point, we know something went wrong in the remotefs program, but + // we we don't know why. + return nil, fmt.Errorf("unknown error") + } + + // Successful command, we might have some data to read (for Read + Seek) + buf := make([]byte, hdr.Size, hdr.Size) + if _, err := io.ReadFull(l.stdout, buf); err != nil { + return nil, err + } + return buf, nil +} diff --git a/daemon/graphdriver/lcow/remotefs_filedriver.go b/daemon/graphdriver/lcow/remotefs_filedriver.go new file mode 100644 index 0000000000..a3e0d9e9f5 --- /dev/null +++ b/daemon/graphdriver/lcow/remotefs_filedriver.go @@ -0,0 +1,123 @@ +// +build windows + +package lcow + +import ( + "bytes" + "encoding/json" + "os" + "strconv" + + "github.com/Microsoft/opengcs/service/gcsutils/remotefs" + + "github.com/containerd/continuity/driver" + "github.com/sirupsen/logrus" +) + +var _ driver.Driver = &lcowfs{} + +func (l *lcowfs) Readlink(p string) (string, error) { + logrus.Debugf("removefs.readlink args: %s", p) + + result := &bytes.Buffer{} + if err := l.runRemoteFSProcess(nil, result, remotefs.ReadlinkCmd, p); err != nil { + return "", err + } + return result.String(), nil +} + +func (l *lcowfs) Mkdir(path string, mode os.FileMode) error { + return l.mkdir(path, mode, remotefs.MkdirCmd) +} + +func (l *lcowfs) MkdirAll(path string, mode os.FileMode) error { + return l.mkdir(path, mode, remotefs.MkdirAllCmd) +} + +func (l *lcowfs) mkdir(path string, mode os.FileMode, cmd string) error { + modeStr := strconv.FormatUint(uint64(mode), 8) + logrus.Debugf("remotefs.%s args: %s %s", cmd, path, modeStr) + return l.runRemoteFSProcess(nil, nil, cmd, path, modeStr) +} + +func (l *lcowfs) Remove(path string) error { + return l.remove(path, remotefs.RemoveCmd) +} + +func (l *lcowfs) RemoveAll(path string) error { + return l.remove(path, remotefs.RemoveAllCmd) +} + +func (l *lcowfs) remove(path string, cmd string) error { + logrus.Debugf("remotefs.%s args: %s", cmd, path) + return l.runRemoteFSProcess(nil, nil, cmd, path) +} + +func (l *lcowfs) Link(oldname, newname string) error { + return l.link(oldname, newname, remotefs.LinkCmd) +} + +func (l *lcowfs) Symlink(oldname, newname string) error { + return l.link(oldname, newname, remotefs.SymlinkCmd) +} + +func (l *lcowfs) link(oldname, newname, cmd string) error { + logrus.Debugf("remotefs.%s args: %s %s", cmd, oldname, newname) + return l.runRemoteFSProcess(nil, nil, cmd, oldname, newname) +} + +func (l *lcowfs) Lchown(name string, uid, gid int64) error { + uidStr := strconv.FormatInt(uid, 10) + gidStr := strconv.FormatInt(gid, 10) + + logrus.Debugf("remotefs.lchown args: %s %s %s", name, uidStr, gidStr) + return l.runRemoteFSProcess(nil, nil, remotefs.LchownCmd, name, uidStr, gidStr) +} + +// Lchmod changes the mode of an file not following symlinks. +func (l *lcowfs) Lchmod(path string, mode os.FileMode) error { + modeStr := strconv.FormatUint(uint64(mode), 8) + logrus.Debugf("remotefs.lchmod args: %s %s", path, modeStr) + return l.runRemoteFSProcess(nil, nil, remotefs.LchmodCmd, path, modeStr) +} + +func (l *lcowfs) Mknod(path string, mode os.FileMode, major, minor int) error { + modeStr := strconv.FormatUint(uint64(mode), 8) + majorStr := strconv.FormatUint(uint64(major), 10) + minorStr := strconv.FormatUint(uint64(minor), 10) + + logrus.Debugf("remotefs.mknod args: %s %s %s %s", path, modeStr, majorStr, minorStr) + return l.runRemoteFSProcess(nil, nil, remotefs.MknodCmd, path, modeStr, majorStr, minorStr) +} + +func (l *lcowfs) Mkfifo(path string, mode os.FileMode) error { + modeStr := strconv.FormatUint(uint64(mode), 8) + logrus.Debugf("remotefs.mkfifo args: %s %s", path, modeStr) + return l.runRemoteFSProcess(nil, nil, remotefs.MkfifoCmd, path, modeStr) +} + +func (l *lcowfs) Stat(p string) (os.FileInfo, error) { + return l.stat(p, remotefs.StatCmd) +} + +func (l *lcowfs) Lstat(p string) (os.FileInfo, error) { + return l.stat(p, remotefs.LstatCmd) +} + +func (l *lcowfs) stat(path string, cmd string) (os.FileInfo, error) { + logrus.Debugf("remotefs.stat inputs: %s %s", cmd, path) + + output := &bytes.Buffer{} + err := l.runRemoteFSProcess(nil, output, cmd, path) + if err != nil { + return nil, err + } + + var fi remotefs.FileInfo + if err := json.Unmarshal(output.Bytes(), &fi); err != nil { + return nil, err + } + + logrus.Debugf("remotefs.stat success. got: %v\n", fi) + return &fi, nil +} diff --git a/daemon/graphdriver/lcow/remotefs_pathdriver.go b/daemon/graphdriver/lcow/remotefs_pathdriver.go new file mode 100644 index 0000000000..95d3e715a2 --- /dev/null +++ b/daemon/graphdriver/lcow/remotefs_pathdriver.go @@ -0,0 +1,212 @@ +// +build windows + +package lcow + +import ( + "errors" + "os" + pathpkg "path" + "path/filepath" + "sort" + "strings" + + "github.com/containerd/continuity/pathdriver" +) + +var _ pathdriver.PathDriver = &lcowfs{} + +// Continuity Path functions can be done locally +func (l *lcowfs) Join(path ...string) string { + return pathpkg.Join(path...) +} + +func (l *lcowfs) IsAbs(path string) bool { + return pathpkg.IsAbs(path) +} + +func sameWord(a, b string) bool { + return a == b +} + +// Implementation taken from the Go standard library +func (l *lcowfs) Rel(basepath, targpath string) (string, error) { + baseVol := "" + targVol := "" + base := l.Clean(basepath) + targ := l.Clean(targpath) + if sameWord(targ, base) { + return ".", nil + } + base = base[len(baseVol):] + targ = targ[len(targVol):] + if base == "." { + base = "" + } + // Can't use IsAbs - `\a` and `a` are both relative in Windows. + baseSlashed := len(base) > 0 && base[0] == l.Separator() + targSlashed := len(targ) > 0 && targ[0] == l.Separator() + if baseSlashed != targSlashed || !sameWord(baseVol, targVol) { + return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) + } + // Position base[b0:bi] and targ[t0:ti] at the first differing elements. + bl := len(base) + tl := len(targ) + var b0, bi, t0, ti int + for { + for bi < bl && base[bi] != l.Separator() { + bi++ + } + for ti < tl && targ[ti] != l.Separator() { + ti++ + } + if !sameWord(targ[t0:ti], base[b0:bi]) { + break + } + if bi < bl { + bi++ + } + if ti < tl { + ti++ + } + b0 = bi + t0 = ti + } + if base[b0:bi] == ".." { + return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) + } + if b0 != bl { + // Base elements left. Must go up before going down. + seps := strings.Count(base[b0:bl], string(l.Separator())) + size := 2 + seps*3 + if tl != t0 { + size += 1 + tl - t0 + } + buf := make([]byte, size) + n := copy(buf, "..") + for i := 0; i < seps; i++ { + buf[n] = l.Separator() + copy(buf[n+1:], "..") + n += 3 + } + if t0 != tl { + buf[n] = l.Separator() + copy(buf[n+1:], targ[t0:]) + } + return string(buf), nil + } + return targ[t0:], nil +} + +func (l *lcowfs) Base(path string) string { + return pathpkg.Base(path) +} + +func (l *lcowfs) Dir(path string) string { + return pathpkg.Dir(path) +} + +func (l *lcowfs) Clean(path string) string { + return pathpkg.Clean(path) +} + +func (l *lcowfs) Split(path string) (dir, file string) { + return pathpkg.Split(path) +} + +func (l *lcowfs) Separator() byte { + return '/' +} + +func (l *lcowfs) Abs(path string) (string, error) { + // Abs is supposed to add the current working directory, which is meaningless in lcow. + // So, return an error. + return "", ErrNotSupported +} + +// Implementation taken from the Go standard library +func (l *lcowfs) Walk(root string, walkFn filepath.WalkFunc) error { + info, err := l.Lstat(root) + if err != nil { + err = walkFn(root, nil, err) + } else { + err = l.walk(root, info, walkFn) + } + if err == filepath.SkipDir { + return nil + } + return err +} + +// walk recursively descends path, calling w. +func (l *lcowfs) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { + err := walkFn(path, info, nil) + if err != nil { + if info.IsDir() && err == filepath.SkipDir { + return nil + } + return err + } + + if !info.IsDir() { + return nil + } + + names, err := l.readDirNames(path) + if err != nil { + return walkFn(path, info, err) + } + + for _, name := range names { + filename := l.Join(path, name) + fileInfo, err := l.Lstat(filename) + if err != nil { + if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { + return err + } + } else { + err = l.walk(filename, fileInfo, walkFn) + if err != nil { + if !fileInfo.IsDir() || err != filepath.SkipDir { + return err + } + } + } + } + return nil +} + +// readDirNames reads the directory named by dirname and returns +// a sorted list of directory entries. +func (l *lcowfs) readDirNames(dirname string) ([]string, error) { + f, err := l.Open(dirname) + if err != nil { + return nil, err + } + files, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + + names := make([]string, len(files), len(files)) + for i := range files { + names[i] = files[i].Name() + } + + sort.Strings(names) + return names, nil +} + +// Note that Go's filepath.FromSlash/ToSlash convert between OS paths and '/'. Since the path separator +// for LCOW (and Unix) is '/', they are no-ops. +func (l *lcowfs) FromSlash(path string) string { + return path +} + +func (l *lcowfs) ToSlash(path string) string { + return path +} + +func (l *lcowfs) Match(pattern, name string) (matched bool, err error) { + return pathpkg.Match(pattern, name) +} diff --git a/daemon/graphdriver/overlay/overlay.go b/daemon/graphdriver/overlay/overlay.go index a4b03cfd44..9012722c20 100644 --- a/daemon/graphdriver/overlay/overlay.go +++ b/daemon/graphdriver/overlay/overlay.go @@ -15,6 +15,7 @@ import ( "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/daemon/graphdriver/overlayutils" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/fsutils" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/locker" @@ -341,21 +342,21 @@ func (d *Driver) Remove(id string) error { } // Get creates and mounts the required file system for the given id and returns the mount path. -func (d *Driver) Get(id string, mountLabel string) (s string, err error) { +func (d *Driver) Get(id, mountLabel string) (_ containerfs.ContainerFS, err error) { d.locker.Lock(id) defer d.locker.Unlock(id) dir := d.dir(id) if _, err := os.Stat(dir); err != nil { - return "", err + return nil, err } // If id has a root, just return it rootDir := path.Join(dir, "root") if _, err := os.Stat(rootDir); err == nil { - return rootDir, nil + return containerfs.NewLocalContainerFS(rootDir), nil } mergedDir := path.Join(dir, "merged") if count := d.ctr.Increment(mergedDir); count > 1 { - return mergedDir, nil + return containerfs.NewLocalContainerFS(mergedDir), nil } defer func() { if err != nil { @@ -366,7 +367,7 @@ func (d *Driver) Get(id string, mountLabel string) (s string, err error) { }() lowerID, err := ioutil.ReadFile(path.Join(dir, "lower-id")) if err != nil { - return "", err + return nil, err } var ( lowerDir = path.Join(d.dir(string(lowerID)), "root") @@ -375,18 +376,18 @@ func (d *Driver) Get(id string, mountLabel string) (s string, err error) { opts = fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", lowerDir, upperDir, workDir) ) if err := unix.Mount("overlay", mergedDir, "overlay", 0, label.FormatMountLabel(opts, mountLabel)); err != nil { - return "", fmt.Errorf("error creating overlay mount to %s: %v", mergedDir, err) + return nil, fmt.Errorf("error creating overlay mount to %s: %v", mergedDir, err) } // chown "workdir/work" to the remapped root UID/GID. Overlay fs inside a // user namespace requires this to move a directory from lower to upper. rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) if err != nil { - return "", err + return nil, err } if err := os.Chown(path.Join(workDir, "work"), rootUID, rootGID); err != nil { - return "", err + return nil, err } - return mergedDir, nil + return containerfs.NewLocalContainerFS(mergedDir), nil } // Put unmounts the mount path created for the give id. diff --git a/daemon/graphdriver/overlay2/overlay.go b/daemon/graphdriver/overlay2/overlay.go index 6b0d4be869..9650975b3c 100644 --- a/daemon/graphdriver/overlay2/overlay.go +++ b/daemon/graphdriver/overlay2/overlay.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/daemon/graphdriver/quota" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/directory" "github.com/docker/docker/pkg/fsutils" "github.com/docker/docker/pkg/idtools" @@ -514,12 +515,12 @@ func (d *Driver) Remove(id string) error { } // Get creates and mounts the required file system for the given id and returns the mount path. -func (d *Driver) Get(id string, mountLabel string) (s string, err error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { d.locker.Lock(id) defer d.locker.Unlock(id) dir := d.dir(id) if _, err := os.Stat(dir); err != nil { - return "", err + return nil, err } diffDir := path.Join(dir, "diff") @@ -527,14 +528,14 @@ func (d *Driver) Get(id string, mountLabel string) (s string, err error) { if err != nil { // If no lower, just return diff directory if os.IsNotExist(err) { - return diffDir, nil + return containerfs.NewLocalContainerFS(diffDir), nil } - return "", err + return nil, err } mergedDir := path.Join(dir, "merged") if count := d.ctr.Increment(mergedDir); count > 1 { - return mergedDir, nil + return containerfs.NewLocalContainerFS(mergedDir), nil } defer func() { if err != nil { @@ -574,7 +575,7 @@ func (d *Driver) Get(id string, mountLabel string) (s string, err error) { opts = fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", string(lowers), path.Join(id, "diff"), path.Join(id, "work")) mountData = label.FormatMountLabel(opts, mountLabel) if len(mountData) > pageSize { - return "", fmt.Errorf("cannot mount layer, mount label too large %d", len(mountData)) + return nil, fmt.Errorf("cannot mount layer, mount label too large %d", len(mountData)) } mount = func(source string, target string, mType string, flags uintptr, label string) error { @@ -584,21 +585,21 @@ func (d *Driver) Get(id string, mountLabel string) (s string, err error) { } if err := mount("overlay", mountTarget, "overlay", 0, mountData); err != nil { - return "", fmt.Errorf("error creating overlay mount to %s: %v", mergedDir, err) + return nil, fmt.Errorf("error creating overlay mount to %s: %v", mergedDir, err) } // chown "workdir/work" to the remapped root UID/GID. Overlay fs inside a // user namespace requires this to move a directory from lower to upper. rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) if err != nil { - return "", err + return nil, err } if err := os.Chown(path.Join(workDir, "work"), rootUID, rootGID); err != nil { - return "", err + return nil, err } - return mergedDir, nil + return containerfs.NewLocalContainerFS(mergedDir), nil } // Put unmounts the mount path created for the give id. diff --git a/daemon/graphdriver/proxy.go b/daemon/graphdriver/proxy.go index 120afad459..81ef872ad9 100644 --- a/daemon/graphdriver/proxy.go +++ b/daemon/graphdriver/proxy.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/plugingetter" "github.com/docker/docker/pkg/plugins" @@ -129,20 +130,20 @@ func (d *graphDriverProxy) Remove(id string) error { return nil } -func (d *graphDriverProxy) Get(id, mountLabel string) (string, error) { +func (d *graphDriverProxy) Get(id, mountLabel string) (containerfs.ContainerFS, error) { args := &graphDriverRequest{ ID: id, MountLabel: mountLabel, } var ret graphDriverResponse if err := d.p.Client().Call("GraphDriver.Get", args, &ret); err != nil { - return "", err + return nil, err } var err error if ret.Err != "" { err = errors.New(ret.Err) } - return filepath.Join(d.p.BasePath(), ret.Dir), err + return containerfs.NewLocalContainerFS(filepath.Join(d.p.BasePath(), ret.Dir)), err } func (d *graphDriverProxy) Put(id string) error { diff --git a/daemon/graphdriver/vfs/driver.go b/daemon/graphdriver/vfs/driver.go index f67ada3582..0482dccb87 100644 --- a/daemon/graphdriver/vfs/driver.go +++ b/daemon/graphdriver/vfs/driver.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/system" "github.com/opencontainers/selinux/go-selinux/label" @@ -94,7 +95,7 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error { if err != nil { return fmt.Errorf("%s: %s", parent, err) } - return CopyWithTar(parentDir, dir) + return CopyWithTar(parentDir.Path(), dir) } func (d *Driver) dir(id string) string { @@ -107,14 +108,14 @@ func (d *Driver) Remove(id string) error { } // Get returns the directory for the given id. -func (d *Driver) Get(id, mountLabel string) (string, error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { dir := d.dir(id) if st, err := os.Stat(dir); err != nil { - return "", err + return nil, err } else if !st.IsDir() { - return "", fmt.Errorf("%s: not a directory", dir) + return nil, fmt.Errorf("%s: not a directory", dir) } - return dir, nil + return containerfs.NewLocalContainerFS(dir), nil } // Put is a noop for vfs that return nil for the error, since this driver has no runtime resources to clean up. diff --git a/daemon/graphdriver/windows/windows.go b/daemon/graphdriver/windows/windows.go index 44114051bb..e7130d80f2 100644 --- a/daemon/graphdriver/windows/windows.go +++ b/daemon/graphdriver/windows/windows.go @@ -26,6 +26,7 @@ import ( "github.com/Microsoft/hcsshim" "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/longpath" @@ -354,36 +355,36 @@ func (d *Driver) Remove(id string) error { } // Get returns the rootfs path for the id. This will mount the dir at its given path. -func (d *Driver) Get(id, mountLabel string) (string, error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { panicIfUsedByLcow() logrus.Debugf("WindowsGraphDriver Get() id %s mountLabel %s", id, mountLabel) var dir string rID, err := d.resolveID(id) if err != nil { - return "", err + return nil, err } if count := d.ctr.Increment(rID); count > 1 { - return d.cache[rID], nil + return containerfs.NewLocalContainerFS(d.cache[rID]), nil } // Getting the layer paths must be done outside of the lock. layerChain, err := d.getLayerChain(rID) if err != nil { d.ctr.Decrement(rID) - return "", err + return nil, err } if err := hcsshim.ActivateLayer(d.info, rID); err != nil { d.ctr.Decrement(rID) - return "", err + return nil, err } if err := hcsshim.PrepareLayer(d.info, rID, layerChain); err != nil { d.ctr.Decrement(rID) if err2 := hcsshim.DeactivateLayer(d.info, rID); err2 != nil { logrus.Warnf("Failed to Deactivate %s: %s", id, err) } - return "", err + return nil, err } mountPath, err := hcsshim.GetLayerMountPath(d.info, rID) @@ -395,7 +396,7 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { if err2 := hcsshim.DeactivateLayer(d.info, rID); err2 != nil { logrus.Warnf("Failed to Deactivate %s: %s", id, err) } - return "", err + return nil, err } d.cacheMu.Lock() d.cache[rID] = mountPath @@ -409,7 +410,7 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { dir = d.dir(id) } - return dir, nil + return containerfs.NewLocalContainerFS(dir), nil } // Put adds a new layer to the driver. @@ -618,7 +619,7 @@ func (d *Driver) DiffSize(id, parent string) (size int64, err error) { } defer d.Put(id) - return archive.ChangesSize(layerFs, changes), nil + return archive.ChangesSize(layerFs.Path(), changes), nil } // GetMetadata returns custom driver information. diff --git a/daemon/graphdriver/zfs/zfs.go b/daemon/graphdriver/zfs/zfs.go index 1bfdfbc7da..4caedef0ee 100644 --- a/daemon/graphdriver/zfs/zfs.go +++ b/daemon/graphdriver/zfs/zfs.go @@ -13,6 +13,7 @@ import ( "time" "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/parsers" @@ -356,10 +357,10 @@ func (d *Driver) Remove(id string) error { } // Get returns the mountpoint for the given id after creating the target directories if necessary. -func (d *Driver) Get(id, mountLabel string) (string, error) { +func (d *Driver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { mountpoint := d.mountPath(id) if count := d.ctr.Increment(mountpoint); count > 1 { - return mountpoint, nil + return containerfs.NewLocalContainerFS(mountpoint), nil } filesystem := d.zfsPath(id) @@ -369,17 +370,17 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) if err != nil { d.ctr.Decrement(mountpoint) - return "", err + return nil, err } // Create the target directories if they don't exist if err := idtools.MkdirAllAs(mountpoint, 0755, rootUID, rootGID); err != nil { d.ctr.Decrement(mountpoint) - return "", err + return nil, err } if err := mount.Mount(filesystem, mountpoint, "zfs", options); err != nil { d.ctr.Decrement(mountpoint) - return "", fmt.Errorf("error creating zfs mount of %s to %s: %v", filesystem, mountpoint, err) + return nil, fmt.Errorf("error creating zfs mount of %s to %s: %v", filesystem, mountpoint, err) } // this could be our first mount after creation of the filesystem, and the root dir may still have root @@ -387,10 +388,10 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { if err := os.Chown(mountpoint, rootUID, rootGID); err != nil { mount.Unmount(mountpoint) d.ctr.Decrement(mountpoint) - return "", fmt.Errorf("error modifying zfs mountpoint (%s) directory ownership: %v", mountpoint, err) + return nil, fmt.Errorf("error modifying zfs mountpoint (%s) directory ownership: %v", mountpoint, err) } - return mountpoint, nil + return containerfs.NewLocalContainerFS(mountpoint), nil } // Put removes the existing mountpoint for the given id if it exists. diff --git a/daemon/initlayer/setup_solaris.go b/daemon/initlayer/setup_solaris.go index 66d53f0eef..57bc116b4b 100644 --- a/daemon/initlayer/setup_solaris.go +++ b/daemon/initlayer/setup_solaris.go @@ -2,12 +2,14 @@ package initlayer +import "github.com/docker/docker/pkg/containerfs" + // Setup populates a directory with mountpoints suitable // for bind-mounting dockerinit into the container. The mountpoint is simply an // empty file at /.dockerinit // // This extra layer is used by all containers as the top-most ro layer. It protects // the container from unwanted side-effects on the rw layer. -func Setup(initLayer string, rootUID, rootGID int) error { +func Setup(initLayer containerfs.ContainerFS, rootUID, rootGID int) error { return nil } diff --git a/daemon/initlayer/setup_unix.go b/daemon/initlayer/setup_unix.go index e26d3a05f1..a02cea6f37 100644 --- a/daemon/initlayer/setup_unix.go +++ b/daemon/initlayer/setup_unix.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "golang.org/x/sys/unix" ) @@ -16,7 +17,10 @@ import ( // // This extra layer is used by all containers as the top-most ro layer. It protects // the container from unwanted side-effects on the rw layer. -func Setup(initLayer string, rootIDs idtools.IDPair) error { +func Setup(initLayerFs containerfs.ContainerFS, rootIDs idtools.IDPair) error { + // Since all paths are local to the container, we can just extract initLayerFs.Path() + initLayer := initLayerFs.Path() + for pth, typ := range map[string]string{ "/dev/pts": "dir", "/dev/shm": "dir", diff --git a/daemon/initlayer/setup_windows.go b/daemon/initlayer/setup_windows.go index 2b22f58b5e..b47563ebf6 100644 --- a/daemon/initlayer/setup_windows.go +++ b/daemon/initlayer/setup_windows.go @@ -3,6 +3,7 @@ package initlayer import ( + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" ) @@ -12,6 +13,6 @@ import ( // // This extra layer is used by all containers as the top-most ro layer. It protects // the container from unwanted side-effects on the rw layer. -func Setup(initLayer string, rootIDs idtools.IDPair) error { +func Setup(initLayer containerfs.ContainerFS, rootIDs idtools.IDPair) error { return nil } diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index 3d411bb251..a8ff57dc44 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -19,7 +19,6 @@ import ( "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/stringutils" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/volume" "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/cgroups" @@ -187,7 +186,7 @@ func setUser(s *specs.Spec, c *container.Container) error { } func readUserFile(c *container.Container, p string) (io.ReadCloser, error) { - fp, err := symlink.FollowSymlinkInScope(filepath.Join(c.BaseFS, p), c.BaseFS) + fp, err := c.GetResourcePath(p) if err != nil { return nil, err } @@ -632,7 +631,7 @@ func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container) return err } s.Root = &specs.Root{ - Path: c.BaseFS, + Path: c.BaseFS.Path(), Readonly: c.HostConfig.ReadonlyRootfs, } if err := c.SetupWorkingDirectory(daemon.idMappings.RootPair()); err != nil { diff --git a/daemon/oci_solaris.go b/daemon/oci_solaris.go index 610efe10a1..45fa1e0ffe 100644 --- a/daemon/oci_solaris.go +++ b/daemon/oci_solaris.go @@ -2,7 +2,6 @@ package daemon import ( "fmt" - "path/filepath" "sort" "strconv" @@ -127,7 +126,7 @@ func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container) return err } s.Root = specs.Root{ - Path: filepath.Dir(c.BaseFS), + Path: c.BaseFS.Dir(c.BaseFS.Path()), Readonly: c.HostConfig.ReadonlyRootfs, } if err := c.SetupWorkingDirectory(daemon.idMappings.RootPair()); err != nil { diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index 0254351569..c917d176fc 100644 --- a/daemon/oci_windows.go +++ b/daemon/oci_windows.go @@ -233,7 +233,7 @@ func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.S s.Root.Readonly = false // Windows does not support a read-only root filesystem if !isHyperV { - s.Root.Path = c.BaseFS // This is not set for Hyper-V containers + s.Root.Path = c.BaseFS.Path() // This is not set for Hyper-V containers if !strings.HasSuffix(s.Root.Path, `\`) { s.Root.Path = s.Root.Path + `\` // Ensure a correctly formatted volume GUID path \\?\Volume{GUID}\ } diff --git a/daemon/start.go b/daemon/start.go index 55438cf2c4..de32a649d7 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -204,7 +204,7 @@ func (daemon *Daemon) Cleanup(container *container.Container) { daemon.unregisterExecCommand(container, eConfig) } - if container.BaseFS != "" { + if container.BaseFS != nil && container.BaseFS.Path() != "" { if err := container.UnmountVolumes(daemon.LogVolumeEvent); err != nil { logrus.Warnf("%s cleanup: Failed to umount volumes: %v", container.ID, err) } diff --git a/integration-cli/docker_cli_external_graphdriver_unix_test.go b/integration-cli/docker_cli_external_graphdriver_unix_test.go index 16023c9a75..8e766bcc31 100644 --- a/integration-cli/docker_cli_external_graphdriver_unix_test.go +++ b/integration-cli/docker_cli_external_graphdriver_unix_test.go @@ -198,12 +198,13 @@ func (s *DockerExternalGraphdriverSuite) setUpPlugin(c *check.C, name string, ex return } + // TODO @gupta-ak: Figure out what to do here. dir, err := driver.Get(req.ID, req.MountLabel) if err != nil { respond(w, err) return } - respond(w, &graphDriverResponse{Dir: dir}) + respond(w, &graphDriverResponse{Dir: dir.Path()}) }) mux.HandleFunc("/GraphDriver.Put", func(w http.ResponseWriter, r *http.Request) { diff --git a/layer/layer.go b/layer/layer.go index e269ef8a4a..4ff159e9f8 100644 --- a/layer/layer.go +++ b/layer/layer.go @@ -15,6 +15,7 @@ import ( "github.com/docker/distribution" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" ) @@ -137,7 +138,7 @@ type RWLayer interface { // Mount mounts the RWLayer and returns the filesystem path // the to the writable layer. - Mount(mountLabel string) (string, error) + Mount(mountLabel string) (containerfs.ContainerFS, error) // Unmount unmounts the RWLayer. This should be called // for every mount. If there are multiple mount calls @@ -178,7 +179,7 @@ type Metadata struct { // writable mount. Changes made here will // not be included in the Tar stream of the // RWLayer. -type MountInit func(root string) error +type MountInit func(root containerfs.ContainerFS) error // CreateRWLayerOpts contains optional arguments to be passed to CreateRWLayer type CreateRWLayerOpts struct { diff --git a/layer/layer_store.go b/layer/layer_store.go index 7283014459..c3973cef70 100644 --- a/layer/layer_store.go +++ b/layer/layer_store.go @@ -749,5 +749,5 @@ func (n *naiveDiffPathDriver) DiffGetter(id string) (graphdriver.FileGetCloser, if err != nil { return nil, err } - return &fileGetPutter{storage.NewPathFileGetter(p), n.Driver, id}, nil + return &fileGetPutter{storage.NewPathFileGetter(p.Path()), n.Driver, id}, nil } diff --git a/layer/layer_test.go b/layer/layer_test.go index 8ec5b4df54..5839ac3852 100644 --- a/layer/layer_test.go +++ b/layer/layer_test.go @@ -10,9 +10,11 @@ import ( "strings" "testing" + "github.com/containerd/continuity/driver" "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/daemon/graphdriver/vfs" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/stringid" "github.com/opencontainers/go-digest" @@ -82,7 +84,7 @@ func newTestStore(t *testing.T) (Store, string, func()) { } } -type layerInit func(root string) error +type layerInit func(root containerfs.ContainerFS) error func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) { containerID := stringid.GenerateRandomID() @@ -91,12 +93,12 @@ func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) { return nil, err } - path, err := mount.Mount("") + pathFS, err := mount.Mount("") if err != nil { return nil, err } - if err := layerFunc(path); err != nil { + if err := layerFunc(pathFS); err != nil { return nil, err } @@ -123,7 +125,7 @@ func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) { } type FileApplier interface { - ApplyFile(root string) error + ApplyFile(root containerfs.ContainerFS) error } type testFile struct { @@ -140,25 +142,25 @@ func newTestFile(name string, content []byte, perm os.FileMode) FileApplier { } } -func (tf *testFile) ApplyFile(root string) error { - fullPath := filepath.Join(root, tf.name) - if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { +func (tf *testFile) ApplyFile(root containerfs.ContainerFS) error { + fullPath := root.Join(root.Path(), tf.name) + if err := root.MkdirAll(root.Dir(fullPath), 0755); err != nil { return err } // Check if already exists - if stat, err := os.Stat(fullPath); err == nil && stat.Mode().Perm() != tf.permission { - if err := os.Chmod(fullPath, tf.permission); err != nil { + if stat, err := root.Stat(fullPath); err == nil && stat.Mode().Perm() != tf.permission { + if err := root.Lchmod(fullPath, tf.permission); err != nil { return err } } - if err := ioutil.WriteFile(fullPath, tf.content, tf.permission); err != nil { + if err := driver.WriteFile(root, fullPath, tf.content, tf.permission); err != nil { return err } return nil } func initWithFiles(files ...FileApplier) layerInit { - return func(root string) error { + return func(root containerfs.ContainerFS) error { for _, f := range files { if err := f.ApplyFile(root); err != nil { return err @@ -288,7 +290,7 @@ func TestMountAndRegister(t *testing.T) { t.Fatal(err) } - b, err := ioutil.ReadFile(filepath.Join(path2, "testfile.txt")) + b, err := driver.ReadFile(path2, path2.Join(path2.Path(), "testfile.txt")) if err != nil { t.Fatal(err) } @@ -391,12 +393,12 @@ func TestStoreRestore(t *testing.T) { t.Fatal(err) } - path, err := m.Mount("") + pathFS, err := m.Mount("") if err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(filepath.Join(path, "testfile.txt"), []byte("nothing here"), 0644); err != nil { + if err := driver.WriteFile(pathFS, pathFS.Join(pathFS.Path(), "testfile.txt"), []byte("nothing here"), 0644); err != nil { t.Fatal(err) } @@ -430,20 +432,20 @@ func TestStoreRestore(t *testing.T) { if mountPath, err := m2.Mount(""); err != nil { t.Fatal(err) - } else if path != mountPath { - t.Fatalf("Unexpected path %s, expected %s", mountPath, path) + } else if pathFS.Path() != mountPath.Path() { + t.Fatalf("Unexpected path %s, expected %s", mountPath.Path(), pathFS.Path()) } if mountPath, err := m2.Mount(""); err != nil { t.Fatal(err) - } else if path != mountPath { - t.Fatalf("Unexpected path %s, expected %s", mountPath, path) + } else if pathFS.Path() != mountPath.Path() { + t.Fatalf("Unexpected path %s, expected %s", mountPath.Path(), pathFS.Path()) } if err := m2.Unmount(); err != nil { t.Fatal(err) } - b, err := ioutil.ReadFile(filepath.Join(path, "testfile.txt")) + b, err := driver.ReadFile(pathFS, pathFS.Join(pathFS.Path(), "testfile.txt")) if err != nil { t.Fatal(err) } @@ -618,7 +620,7 @@ func tarFromFiles(files ...FileApplier) ([]byte, error) { defer os.RemoveAll(td) for _, f := range files { - if err := f.ApplyFile(td); err != nil { + if err := f.ApplyFile(containerfs.NewLocalContainerFS(td)); err != nil { return nil, err } } diff --git a/layer/layer_windows.go b/layer/layer_windows.go index a1c195311e..d02d4d0dda 100644 --- a/layer/layer_windows.go +++ b/layer/layer_windows.go @@ -1,6 +1,15 @@ package layer -import "errors" +import ( + "errors" +) + +// Getter is an interface to get the path to a layer on the host. +type Getter interface { + // GetLayerPath gets the path for the layer. This is different from Get() + // since that returns an interface to account for umountable layers. + GetLayerPath(id string) (string, error) +} // GetLayerPath returns the path to a layer func GetLayerPath(s Store, layer ChainID) (string, error) { @@ -16,6 +25,10 @@ func GetLayerPath(s Store, layer ChainID) (string, error) { return "", ErrLayerDoesNotExist } + if layerGetter, ok := ls.driver.(Getter); ok { + return layerGetter.GetLayerPath(rl.cacheID) + } + path, err := ls.driver.Get(rl.cacheID, "") if err != nil { return "", err @@ -25,7 +38,7 @@ func GetLayerPath(s Store, layer ChainID) (string, error) { return "", err } - return path, nil + return path.Path(), nil } func (ls *layerStore) mountID(name string) string { diff --git a/layer/mount_test.go b/layer/mount_test.go index f5799e7cd9..44d461f9b8 100644 --- a/layer/mount_test.go +++ b/layer/mount_test.go @@ -2,13 +2,13 @@ package layer import ( "io/ioutil" - "os" - "path/filepath" "runtime" "sort" "testing" + "github.com/containerd/continuity/driver" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" ) func TestMountInit(t *testing.T) { @@ -28,7 +28,7 @@ func TestMountInit(t *testing.T) { t.Fatal(err) } - mountInit := func(root string) error { + mountInit := func(root containerfs.ContainerFS) error { return initfile.ApplyFile(root) } @@ -40,22 +40,22 @@ func TestMountInit(t *testing.T) { t.Fatal(err) } - path, err := m.Mount("") + pathFS, err := m.Mount("") if err != nil { t.Fatal(err) } - f, err := os.Open(filepath.Join(path, "testfile.txt")) + fi, err := pathFS.Stat(pathFS.Join(pathFS.Path(), "testfile.txt")) + if err != nil { + t.Fatal(err) + } + + f, err := pathFS.Open(pathFS.Join(pathFS.Path(), "testfile.txt")) if err != nil { t.Fatal(err) } defer f.Close() - fi, err := f.Stat() - if err != nil { - t.Fatal(err) - } - b, err := ioutil.ReadAll(f) if err != nil { t.Fatal(err) @@ -88,7 +88,7 @@ func TestMountSize(t *testing.T) { t.Fatal(err) } - mountInit := func(root string) error { + mountInit := func(root containerfs.ContainerFS) error { return newTestFile("file-init", contentInit, 0777).ApplyFile(root) } rwLayerOpts := &CreateRWLayerOpts{ @@ -100,12 +100,12 @@ func TestMountSize(t *testing.T) { t.Fatal(err) } - path, err := m.Mount("") + pathFS, err := m.Mount("") if err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(filepath.Join(path, "file2"), content2, 0755); err != nil { + if err := driver.WriteFile(pathFS, pathFS.Join(pathFS.Path(), "file2"), content2, 0755); err != nil { t.Fatal(err) } @@ -140,7 +140,7 @@ func TestMountChanges(t *testing.T) { t.Fatal(err) } - mountInit := func(root string) error { + mountInit := func(root containerfs.ContainerFS) error { return initfile.ApplyFile(root) } rwLayerOpts := &CreateRWLayerOpts{ @@ -152,28 +152,28 @@ func TestMountChanges(t *testing.T) { t.Fatal(err) } - path, err := m.Mount("") + pathFS, err := m.Mount("") if err != nil { t.Fatal(err) } - if err := os.Chmod(filepath.Join(path, "testfile1.txt"), 0755); err != nil { + if err := pathFS.Lchmod(pathFS.Join(pathFS.Path(), "testfile1.txt"), 0755); err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(filepath.Join(path, "testfile1.txt"), []byte("mount data!"), 0755); err != nil { + if err := driver.WriteFile(pathFS, pathFS.Join(pathFS.Path(), "testfile1.txt"), []byte("mount data!"), 0755); err != nil { t.Fatal(err) } - if err := os.Remove(filepath.Join(path, "testfile2.txt")); err != nil { + if err := pathFS.Remove(pathFS.Join(pathFS.Path(), "testfile2.txt")); err != nil { t.Fatal(err) } - if err := os.Chmod(filepath.Join(path, "testfile3.txt"), 0755); err != nil { + if err := pathFS.Lchmod(pathFS.Join(pathFS.Path(), "testfile3.txt"), 0755); err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(filepath.Join(path, "testfile4.txt"), []byte("mount data!"), 0644); err != nil { + if err := driver.WriteFile(pathFS, pathFS.Join(pathFS.Path(), "testfile4.txt"), []byte("mount data!"), 0644); err != nil { t.Fatal(err) } diff --git a/layer/mounted_layer.go b/layer/mounted_layer.go index a5cfcfa9bd..47ef966987 100644 --- a/layer/mounted_layer.go +++ b/layer/mounted_layer.go @@ -4,6 +4,7 @@ import ( "io" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" ) type mountedLayer struct { @@ -88,7 +89,7 @@ type referencedRWLayer struct { *mountedLayer } -func (rl *referencedRWLayer) Mount(mountLabel string) (string, error) { +func (rl *referencedRWLayer) Mount(mountLabel string) (containerfs.ContainerFS, error) { return rl.layerStore.driver.Get(rl.mountedLayer.mountID, mountLabel) } diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 677c1e41c5..876e605680 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -55,18 +55,17 @@ type ( } ) -// Archiver allows the reuse of most utility functions of this package -// with a pluggable Untar function. Also, to facilitate the passing of -// specific id mappings for untar, an archiver can be created with maps -// which will then be passed to Untar operations +// Archiver implements the Archiver interface and allows the reuse of most utility functions of +// this package with a pluggable Untar function. Also, to facilitate the passing of specific id +// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations. type Archiver struct { - Untar func(io.Reader, string, *TarOptions) error - IDMappings *idtools.IDMappings + Untar func(io.Reader, string, *TarOptions) error + IDMappingsVar *idtools.IDMappings } // NewDefaultArchiver returns a new Archiver without any IDMappings func NewDefaultArchiver() *Archiver { - return &Archiver{Untar: Untar, IDMappings: &idtools.IDMappings{}} + return &Archiver{Untar: Untar, IDMappingsVar: &idtools.IDMappings{}} } // breakoutError is used to differentiate errors related to breaking out @@ -1025,8 +1024,8 @@ func (archiver *Archiver) TarUntar(src, dst string) error { } defer archive.Close() options := &TarOptions{ - UIDMaps: archiver.IDMappings.UIDs(), - GIDMaps: archiver.IDMappings.GIDs(), + UIDMaps: archiver.IDMappingsVar.UIDs(), + GIDMaps: archiver.IDMappingsVar.GIDs(), } return archiver.Untar(archive, dst, options) } @@ -1039,8 +1038,8 @@ func (archiver *Archiver) UntarPath(src, dst string) error { } defer archive.Close() options := &TarOptions{ - UIDMaps: archiver.IDMappings.UIDs(), - GIDMaps: archiver.IDMappings.GIDs(), + UIDMaps: archiver.IDMappingsVar.UIDs(), + GIDMaps: archiver.IDMappingsVar.GIDs(), } return archiver.Untar(archive, dst, options) } @@ -1058,10 +1057,10 @@ func (archiver *Archiver) CopyWithTar(src, dst string) error { return archiver.CopyFileWithTar(src, dst) } - // if this archiver is set up with ID mapping we need to create + // if this Archiver is set up with ID mapping we need to create // the new destination directory with the remapped root UID/GID pair // as owner - rootIDs := archiver.IDMappings.RootPair() + rootIDs := archiver.IDMappingsVar.RootPair() // Create dst, copy src's content into it logrus.Debugf("Creating dest directory: %s", dst) if err := idtools.MkdirAllAndChownNew(dst, 0755, rootIDs); err != nil { @@ -1112,7 +1111,7 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { hdr.Name = filepath.Base(dst) hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) - if err := remapIDs(archiver.IDMappings, hdr); err != nil { + if err := remapIDs(archiver.IDMappingsVar, hdr); err != nil { return err } @@ -1139,6 +1138,11 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { return err } +// IDMappings returns the IDMappings of the archiver. +func (archiver *Archiver) IDMappings() *idtools.IDMappings { + return archiver.IDMappingsVar +} + func remapIDs(idMappings *idtools.IDMappings, hdr *tar.Header) error { ids, err := idMappings.ToHost(idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}) hdr.Uid, hdr.Gid = ids.UID, ids.GID diff --git a/pkg/archive/copy.go b/pkg/archive/copy.go index 3adf8a275c..298eb2ad68 100644 --- a/pkg/archive/copy.go +++ b/pkg/archive/copy.go @@ -27,23 +27,23 @@ var ( // path (from before being processed by utility functions from the path or // filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned // path already ends in a `.` path segment, then another is not added. If the -// clean path already ends in a path separator, then another is not added. -func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string { +// clean path already ends in the separator, then another is not added. +func PreserveTrailingDotOrSeparator(cleanedPath string, originalPath string, sep byte) string { // Ensure paths are in platform semantics - cleanedPath = normalizePath(cleanedPath) - originalPath = normalizePath(originalPath) + cleanedPath = strings.Replace(cleanedPath, "/", string(sep), -1) + originalPath = strings.Replace(originalPath, "/", string(sep), -1) if !specifiesCurrentDir(cleanedPath) && specifiesCurrentDir(originalPath) { - if !hasTrailingPathSeparator(cleanedPath) { + if !hasTrailingPathSeparator(cleanedPath, sep) { // Add a separator if it doesn't already end with one (a cleaned // path would only end in a separator if it is the root). - cleanedPath += string(filepath.Separator) + cleanedPath += string(sep) } cleanedPath += "." } - if !hasTrailingPathSeparator(cleanedPath) && hasTrailingPathSeparator(originalPath) { - cleanedPath += string(filepath.Separator) + if !hasTrailingPathSeparator(cleanedPath, sep) && hasTrailingPathSeparator(originalPath, sep) { + cleanedPath += string(sep) } return cleanedPath @@ -52,14 +52,14 @@ func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string { // assertsDirectory returns whether the given path is // asserted to be a directory, i.e., the path ends with // a trailing '/' or `/.`, assuming a path separator of `/`. -func assertsDirectory(path string) bool { - return hasTrailingPathSeparator(path) || specifiesCurrentDir(path) +func assertsDirectory(path string, sep byte) bool { + return hasTrailingPathSeparator(path, sep) || specifiesCurrentDir(path) } // hasTrailingPathSeparator returns whether the given // path ends with the system's path separator character. -func hasTrailingPathSeparator(path string) bool { - return len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) +func hasTrailingPathSeparator(path string, sep byte) bool { + return len(path) > 0 && path[len(path)-1] == sep } // specifiesCurrentDir returns whether the given path specifies @@ -72,10 +72,10 @@ func specifiesCurrentDir(path string) bool { // basename by first cleaning the path but preserves a trailing "." if the // original path specified the current directory. func SplitPathDirEntry(path string) (dir, base string) { - cleanedPath := filepath.Clean(normalizePath(path)) + cleanedPath := filepath.Clean(filepath.FromSlash(path)) if specifiesCurrentDir(path) { - cleanedPath += string(filepath.Separator) + "." + cleanedPath += string(os.PathSeparator) + "." } return filepath.Dir(cleanedPath), filepath.Base(cleanedPath) @@ -106,19 +106,24 @@ func TarResourceRebase(sourcePath, rebaseName string) (content io.ReadCloser, er // Separate the source path between its directory and // the entry in that directory which we are archiving. sourceDir, sourceBase := SplitPathDirEntry(sourcePath) - - filter := []string{sourceBase} + opts := TarResourceRebaseOpts(sourceBase, rebaseName) logrus.Debugf("copying %q from %q", sourceBase, sourceDir) + return TarWithOptions(sourceDir, opts) +} - return TarWithOptions(sourceDir, &TarOptions{ +// TarResourceRebaseOpts does not preform the Tar, but instead just creates the rebase +// parameters to be sent to TarWithOptions (the TarOptions struct) +func TarResourceRebaseOpts(sourceBase string, rebaseName string) *TarOptions { + filter := []string{sourceBase} + return &TarOptions{ Compression: Uncompressed, IncludeFiles: filter, IncludeSourceDir: true, RebaseNames: map[string]string{ sourceBase: rebaseName, }, - }) + } } // CopyInfo holds basic info about the source @@ -281,7 +286,7 @@ func PrepareArchiveCopy(srcContent io.Reader, srcInfo, dstInfo CopyInfo) (dstDir srcBase = srcInfo.RebaseName } return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil - case assertsDirectory(dstInfo.Path): + case assertsDirectory(dstInfo.Path, os.PathSeparator): // The destination does not exist and is asserted to be created as a // directory, but the source content is not a directory. This is an // error condition since you cannot create a directory from a file @@ -351,6 +356,9 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read return rebased } +// TODO @gupta-ak. These might have to be changed in the future to be +// continuity driver aware as well to support LCOW. + // CopyResource performs an archive copy from the given source path to the // given destination path. The source path MUST exist and the destination // path's parent directory must exist. @@ -365,8 +373,8 @@ func CopyResource(srcPath, dstPath string, followLink bool) error { dstPath = normalizePath(dstPath) // Clean the source and destination paths. - srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath) - dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath) + srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath, os.PathSeparator) + dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath, os.PathSeparator) if srcInfo, err = CopyInfoSourcePath(srcPath, followLink); err != nil { return err @@ -429,7 +437,8 @@ func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseNa // resolvedDirPath will have been cleaned (no trailing path separators) so // we can manually join it with the base path element. resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath - if hasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) { + if hasTrailingPathSeparator(path, os.PathSeparator) && + filepath.Base(path) != filepath.Base(resolvedPath) { rebaseName = filepath.Base(path) } } @@ -442,11 +451,13 @@ func GetRebaseName(path, resolvedPath string) (string, string) { // linkTarget will have been cleaned (no trailing path separators and dot) so // we can manually join it with them var rebaseName string - if specifiesCurrentDir(path) && !specifiesCurrentDir(resolvedPath) { + if specifiesCurrentDir(path) && + !specifiesCurrentDir(resolvedPath) { resolvedPath += string(filepath.Separator) + "." } - if hasTrailingPathSeparator(path) && !hasTrailingPathSeparator(resolvedPath) { + if hasTrailingPathSeparator(path, os.PathSeparator) && + !hasTrailingPathSeparator(resolvedPath, os.PathSeparator) { resolvedPath += string(filepath.Separator) } diff --git a/pkg/chrootarchive/archive.go b/pkg/chrootarchive/archive.go index 7604418767..d6d07888e8 100644 --- a/pkg/chrootarchive/archive.go +++ b/pkg/chrootarchive/archive.go @@ -16,7 +16,10 @@ func NewArchiver(idMappings *idtools.IDMappings) *archive.Archiver { if idMappings == nil { idMappings = &idtools.IDMappings{} } - return &archive.Archiver{Untar: Untar, IDMappings: idMappings} + return &archive.Archiver{ + Untar: Untar, + IDMappingsVar: idMappings, + } } // Untar reads a stream of bytes from `archive`, parses it as a tar archive, diff --git a/pkg/containerfs/archiver.go b/pkg/containerfs/archiver.go new file mode 100644 index 0000000000..7fffa00036 --- /dev/null +++ b/pkg/containerfs/archiver.go @@ -0,0 +1,194 @@ +package containerfs + +import ( + "archive/tar" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/system" + "github.com/sirupsen/logrus" +) + +// TarFunc provides a function definition for a custom Tar function +type TarFunc func(string, *archive.TarOptions) (io.ReadCloser, error) + +// UntarFunc provides a function definition for a custom Untar function +type UntarFunc func(io.Reader, string, *archive.TarOptions) error + +// Archiver provides a similar implementation of the archive.Archiver package with the rootfs abstraction +type Archiver struct { + SrcDriver Driver + DstDriver Driver + Tar TarFunc + Untar UntarFunc + IDMappingsVar *idtools.IDMappings +} + +// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other. +// If either Tar or Untar fails, TarUntar aborts and returns the error. +func (archiver *Archiver) TarUntar(src, dst string) error { + logrus.Debugf("TarUntar(%s %s)", src, dst) + tarArchive, err := archiver.Tar(src, &archive.TarOptions{Compression: archive.Uncompressed}) + if err != nil { + return err + } + defer tarArchive.Close() + options := &archive.TarOptions{ + UIDMaps: archiver.IDMappingsVar.UIDs(), + GIDMaps: archiver.IDMappingsVar.GIDs(), + } + return archiver.Untar(tarArchive, dst, options) +} + +// UntarPath untar a file from path to a destination, src is the source tar file path. +func (archiver *Archiver) UntarPath(src, dst string) error { + tarArchive, err := archiver.SrcDriver.Open(src) + if err != nil { + return err + } + defer tarArchive.Close() + options := &archive.TarOptions{ + UIDMaps: archiver.IDMappingsVar.UIDs(), + GIDMaps: archiver.IDMappingsVar.GIDs(), + } + return archiver.Untar(tarArchive, dst, options) +} + +// CopyWithTar creates a tar archive of filesystem path `src`, and +// unpacks it at filesystem path `dst`. +// The archive is streamed directly with fixed buffering and no +// intermediary disk IO. +func (archiver *Archiver) CopyWithTar(src, dst string) error { + srcSt, err := archiver.SrcDriver.Stat(src) + if err != nil { + return err + } + if !srcSt.IsDir() { + return archiver.CopyFileWithTar(src, dst) + } + + // if this archiver is set up with ID mapping we need to create + // the new destination directory with the remapped root UID/GID pair + // as owner + rootIDs := archiver.IDMappingsVar.RootPair() + // Create dst, copy src's content into it + if err := idtools.MkdirAllAndChownNew(dst, 0755, rootIDs); err != nil { + return err + } + logrus.Debugf("Calling TarUntar(%s, %s)", src, dst) + return archiver.TarUntar(src, dst) +} + +// CopyFileWithTar emulates the behavior of the 'cp' command-line +// for a single file. It copies a regular file from path `src` to +// path `dst`, and preserves all its metadata. +func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { + logrus.Debugf("CopyFileWithTar(%s, %s)", src, dst) + srcDriver := archiver.SrcDriver + dstDriver := archiver.DstDriver + + srcSt, err := srcDriver.Stat(src) + if err != nil { + return err + } + + if srcSt.IsDir() { + return fmt.Errorf("Can't copy a directory") + } + + // Clean up the trailing slash. This must be done in an operating + // system specific manner. + if dst[len(dst)-1] == dstDriver.Separator() { + dst = dstDriver.Join(dst, srcDriver.Base(src)) + } + + // The original call was system.MkdirAll, which is just + // os.MkdirAll on not-Windows and changed for Windows. + if dstDriver.OS() == "windows" { + // Now we are WCOW + if err := system.MkdirAll(filepath.Dir(dst), 0700, ""); err != nil { + return err + } + } else { + // We can just use the driver.MkdirAll function + if err := dstDriver.MkdirAll(dstDriver.Dir(dst), 0700); err != nil { + return err + } + } + + r, w := io.Pipe() + errC := promise.Go(func() error { + defer w.Close() + + srcF, err := srcDriver.Open(src) + if err != nil { + return err + } + defer srcF.Close() + + hdr, err := tar.FileInfoHeader(srcSt, "") + if err != nil { + return err + } + hdr.Name = dstDriver.Base(dst) + if dstDriver.OS() == "windows" { + hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) + } else { + hdr.Mode = int64(os.FileMode(hdr.Mode)) + } + + if err := remapIDs(archiver.IDMappingsVar, hdr); err != nil { + return err + } + + tw := tar.NewWriter(w) + defer tw.Close() + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := io.Copy(tw, srcF); err != nil { + return err + } + return nil + }) + defer func() { + if er := <-errC; err == nil && er != nil { + err = er + } + }() + + err = archiver.Untar(r, dstDriver.Dir(dst), nil) + if err != nil { + r.CloseWithError(err) + } + return err +} + +// IDMappings returns the IDMappings of the archiver. +func (archiver *Archiver) IDMappings() *idtools.IDMappings { + return archiver.IDMappingsVar +} + +func remapIDs(idMappings *idtools.IDMappings, hdr *tar.Header) error { + ids, err := idMappings.ToHost(idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}) + hdr.Uid, hdr.Gid = ids.UID, ids.GID + return err +} + +// 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 // this 0-ed out tar flags (like link, regular file, directory marker etc.) + permPart := perm & os.ModePerm + noPermPart := perm &^ os.ModePerm + // Add the x bit: make everything +x from windows + permPart |= 0111 + permPart &= 0755 + + return noPermPart | permPart +} diff --git a/pkg/containerfs/containerfs.go b/pkg/containerfs/containerfs.go new file mode 100644 index 0000000000..05842ac64c --- /dev/null +++ b/pkg/containerfs/containerfs.go @@ -0,0 +1,87 @@ +package containerfs + +import ( + "path/filepath" + "runtime" + + "github.com/containerd/continuity/driver" + "github.com/containerd/continuity/pathdriver" + "github.com/docker/docker/pkg/symlink" +) + +// ContainerFS is that represents a root file system +type ContainerFS interface { + // Path returns the path to the root. Note that this may not exist + // on the local system, so the continuity operations must be used + Path() string + + // ResolveScopedPath evaluates the given path scoped to the root. + // For example, if root=/a, and path=/b/c, then this function would return /a/b/c. + // If rawPath is true, then the function will not preform any modifications + // before path resolution. Otherwise, the function will clean the given path + // by making it an absolute path. + ResolveScopedPath(path string, rawPath bool) (string, error) + + Driver +} + +// Driver combines both continuity's Driver and PathDriver interfaces with a Platform +// field to determine the OS. +type Driver interface { + // OS returns the OS where the rootfs is located. Essentially, + // runtime.GOOS for everything aside from LCOW, which is "linux" + OS() string + + // Architecture returns the hardware architecture where the + // container is located. + Architecture() string + + // Driver & PathDriver provide methods to manipulate files & paths + driver.Driver + pathdriver.PathDriver +} + +// NewLocalContainerFS is a helper function to implement daemon's Mount interface +// when the graphdriver mount point is a local path on the machine. +func NewLocalContainerFS(path string) ContainerFS { + return &local{ + path: path, + Driver: driver.LocalDriver, + PathDriver: pathdriver.LocalPathDriver, + } +} + +// NewLocalDriver provides file and path drivers for a local file system. They are +// essentially a wrapper around the `os` and `filepath` functions. +func NewLocalDriver() Driver { + return &local{ + Driver: driver.LocalDriver, + PathDriver: pathdriver.LocalPathDriver, + } +} + +type local struct { + path string + driver.Driver + pathdriver.PathDriver +} + +func (l *local) Path() string { + return l.path +} + +func (l *local) ResolveScopedPath(path string, rawPath bool) (string, error) { + cleanedPath := path + if !rawPath { + cleanedPath = cleanScopedPath(path) + } + return symlink.FollowSymlinkInScope(filepath.Join(l.path, cleanedPath), l.path) +} + +func (l *local) OS() string { + return runtime.GOOS +} + +func (l *local) Architecture() string { + return runtime.GOARCH +} diff --git a/pkg/containerfs/containerfs_unix.go b/pkg/containerfs/containerfs_unix.go new file mode 100644 index 0000000000..fbc418f012 --- /dev/null +++ b/pkg/containerfs/containerfs_unix.go @@ -0,0 +1,10 @@ +// +build !windows + +package containerfs + +import "path/filepath" + +// cleanScopedPath preappends a to combine with a mnt path. +func cleanScopedPath(path string) string { + return filepath.Join(string(filepath.Separator), path) +} diff --git a/pkg/containerfs/containerfs_windows.go b/pkg/containerfs/containerfs_windows.go new file mode 100644 index 0000000000..56f5a7564f --- /dev/null +++ b/pkg/containerfs/containerfs_windows.go @@ -0,0 +1,15 @@ +package containerfs + +import "path/filepath" + +// cleanScopedPath removes the C:\ syntax, and prepares to combine +// with a volume path +func cleanScopedPath(path string) string { + if len(path) >= 2 { + c := path[0] + if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { + path = path[2:] + } + } + return filepath.Join(string(filepath.Separator), path) +} diff --git a/pkg/system/path.go b/pkg/system/path.go index f634a6be67..4160616f43 100644 --- a/pkg/system/path.go +++ b/pkg/system/path.go @@ -1,6 +1,13 @@ package system -import "runtime" +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + + "github.com/containerd/continuity/pathdriver" +) const defaultUnixPathEnv = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" @@ -19,3 +26,35 @@ func DefaultPathEnv(platform string) string { return defaultUnixPathEnv } + +// CheckSystemDriveAndRemoveDriveLetter verifies that a path, if it includes a drive letter, +// is the system drive. +// On Linux: this is a no-op. +// On Windows: this does the following> +// CheckSystemDriveAndRemoveDriveLetter verifies and manipulates a Windows path. +// This is used, for example, when validating a user provided path in docker cp. +// If a drive letter is supplied, it must be the system drive. The drive letter +// is always removed. Also, it translates it to OS semantics (IOW / to \). We +// need the path in this syntax so that it can ultimately be contatenated with +// a Windows long-path which doesn't support drive-letters. Examples: +// C: --> Fail +// C:\ --> \ +// a --> a +// /a --> \a +// d:\ --> Fail +func CheckSystemDriveAndRemoveDriveLetter(path string, driver pathdriver.PathDriver) (string, error) { + if runtime.GOOS != "windows" || LCOWSupported() { + return path, nil + } + + if len(path) == 2 && string(path[1]) == ":" { + return "", fmt.Errorf("No relative path specified in %q", path) + } + if !driver.IsAbs(path) || len(path) < 2 { + return filepath.FromSlash(path), nil + } + if string(path[1]) == ":" && !strings.EqualFold(string(path[0]), "c") { + return "", fmt.Errorf("The specified path is not on the system drive (C:)") + } + return filepath.FromSlash(path[2:]), nil +} diff --git a/pkg/system/path_unix.go b/pkg/system/path_unix.go deleted file mode 100644 index f3762e69d3..0000000000 --- a/pkg/system/path_unix.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !windows - -package system - -// CheckSystemDriveAndRemoveDriveLetter verifies that a path, if it includes a drive letter, -// is the system drive. This is a no-op on Linux. -func CheckSystemDriveAndRemoveDriveLetter(path string) (string, error) { - return path, nil -} diff --git a/pkg/system/path_windows.go b/pkg/system/path_windows.go deleted file mode 100644 index aab891522d..0000000000 --- a/pkg/system/path_windows.go +++ /dev/null @@ -1,33 +0,0 @@ -// +build windows - -package system - -import ( - "fmt" - "path/filepath" - "strings" -) - -// CheckSystemDriveAndRemoveDriveLetter verifies and manipulates a Windows path. -// This is used, for example, when validating a user provided path in docker cp. -// If a drive letter is supplied, it must be the system drive. The drive letter -// is always removed. Also, it translates it to OS semantics (IOW / to \). We -// need the path in this syntax so that it can ultimately be concatenated with -// a Windows long-path which doesn't support drive-letters. Examples: -// C: --> Fail -// C:\ --> \ -// a --> a -// /a --> \a -// d:\ --> Fail -func CheckSystemDriveAndRemoveDriveLetter(path string) (string, error) { - if len(path) == 2 && string(path[1]) == ":" { - return "", fmt.Errorf("No relative path specified in %q", path) - } - if !filepath.IsAbs(path) || len(path) < 2 { - return filepath.FromSlash(path), nil - } - if string(path[1]) == ":" && !strings.EqualFold(string(path[0]), "c") { - return "", fmt.Errorf("The specified path is not on the system drive (C:)") - } - return filepath.FromSlash(path[2:]), nil -} diff --git a/pkg/system/path_windows_test.go b/pkg/system/path_windows_test.go index 8e21765a3c..0e6bcab522 100644 --- a/pkg/system/path_windows_test.go +++ b/pkg/system/path_windows_test.go @@ -2,19 +2,23 @@ package system -import "testing" +import ( + "testing" + + "github.com/containerd/continuity/pathdriver" +) // TestCheckSystemDriveAndRemoveDriveLetter tests CheckSystemDriveAndRemoveDriveLetter func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { // Fails if not C drive. - _, err := CheckSystemDriveAndRemoveDriveLetter(`d:\`) + _, err := CheckSystemDriveAndRemoveDriveLetter(`d:\`, pathdriver.LocalPathDriver) if err == nil || (err != nil && err.Error() != "The specified path is not on the system drive (C:)") { t.Fatalf("Expected error for d:") } // Single character is unchanged var path string - if path, err = CheckSystemDriveAndRemoveDriveLetter("z"); err != nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter("z", pathdriver.LocalPathDriver); err != nil { t.Fatalf("Single character should pass") } if path != "z" { @@ -22,7 +26,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Two characters without colon is unchanged - if path, err = CheckSystemDriveAndRemoveDriveLetter("AB"); err != nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter("AB", pathdriver.LocalPathDriver); err != nil { t.Fatalf("2 characters without colon should pass") } if path != "AB" { @@ -30,7 +34,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Abs path without drive letter - if path, err = CheckSystemDriveAndRemoveDriveLetter(`\l`); err != nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter(`\l`, pathdriver.LocalPathDriver); err != nil { t.Fatalf("abs path no drive letter should pass") } if path != `\l` { @@ -38,7 +42,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Abs path without drive letter, linux style - if path, err = CheckSystemDriveAndRemoveDriveLetter(`/l`); err != nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter(`/l`, pathdriver.LocalPathDriver); err != nil { t.Fatalf("abs path no drive letter linux style should pass") } if path != `\l` { @@ -46,7 +50,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Drive-colon should be stripped - if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:\`); err != nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:\`, pathdriver.LocalPathDriver); err != nil { t.Fatalf("An absolute path should pass") } if path != `\` { @@ -54,7 +58,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Verify with a linux-style path - if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:/`); err != nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:/`, pathdriver.LocalPathDriver); err != nil { t.Fatalf("An absolute path should pass") } if path != `\` { @@ -62,7 +66,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Failure on c: - if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:`); err == nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter(`c:`, pathdriver.LocalPathDriver); err == nil { t.Fatalf("c: should fail") } if err.Error() != `No relative path specified in "c:"` { @@ -70,7 +74,7 @@ func TestCheckSystemDriveAndRemoveDriveLetter(t *testing.T) { } // Failure on d: - if path, err = CheckSystemDriveAndRemoveDriveLetter(`d:`); err == nil { + if path, err = CheckSystemDriveAndRemoveDriveLetter(`d:`, pathdriver.LocalPathDriver); err == nil { t.Fatalf("c: should fail") } if err.Error() != `No relative path specified in "d:"` { diff --git a/plugin/manager_linux.go b/plugin/manager_linux.go index 84bf606346..7c832b55b2 100644 --- a/plugin/manager_linux.go +++ b/plugin/manager_linux.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/daemon/initlayer" "github.com/docker/docker/libcontainerd" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/plugins" @@ -57,7 +58,8 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error { } } - if err := initlayer.Setup(filepath.Join(pm.config.Root, p.PluginObj.ID, rootFSFileName), idtools.IDPair{0, 0}); err != nil { + rootFS := containerfs.NewLocalContainerFS(filepath.Join(pm.config.Root, p.PluginObj.ID, rootFSFileName)) + if err := initlayer.Setup(rootFS, idtools.IDPair{0, 0}); err != nil { return errors.WithStack(err) } diff --git a/vendor.conf b/vendor.conf index 3a42e21e2d..30ad86ea7e 100644 --- a/vendor.conf +++ b/vendor.conf @@ -27,7 +27,7 @@ github.com/RackSec/srslog 456df3a81436d29ba874f3590eeeee25d666f8a5 github.com/imdario/mergo 0.2.1 golang.org/x/sync de49d9dcd27d4f764488181bea099dfe6179bcf0 -github.com/containerd/continuity cf279e6ac893682272b4479d4c67fd3abf878b4e +github.com/containerd/continuity 22694c680ee48fb8f50015b44618517e2bde77e8 #get libnetwork packages github.com/docker/libnetwork d5c822319097cc01cc9bd5ffedd74c7ce7c894f2 diff --git a/vendor/github.com/Microsoft/opengcs/service/gcsutils/README b/vendor/github.com/Microsoft/opengcs/service/gcsutils/README new file mode 100644 index 0000000000..85e54428a0 --- /dev/null +++ b/vendor/github.com/Microsoft/opengcs/service/gcsutils/README @@ -0,0 +1,4 @@ +1. This program only runs in Linux. So you just first copy the files over to a Linux machine. +2. Get Go and and then run make get-deps && make. This is set the $GOPATH for you and build the binaries. +3. vhd_to_tar and tar_to_vhd are the standalone executables that read/write to stdin/out and do the tar <-> vhd conversion. + tar2vhd_server is the service VM server that takes client requests over hvsock. diff --git a/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/defs.go b/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/defs.go new file mode 100644 index 0000000000..f1f2c04a45 --- /dev/null +++ b/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/defs.go @@ -0,0 +1,109 @@ +package remotefs + +import ( + "errors" + "os" + "time" +) + +// RemotefsCmd is the name of the remotefs meta command +const RemotefsCmd = "remotefs" + +// Name of the commands when called from the cli context (remotefs ...) +const ( + StatCmd = "stat" + LstatCmd = "lstat" + ReadlinkCmd = "readlink" + MkdirCmd = "mkdir" + MkdirAllCmd = "mkdirall" + RemoveCmd = "remove" + RemoveAllCmd = "removeall" + LinkCmd = "link" + SymlinkCmd = "symlink" + LchmodCmd = "lchmod" + LchownCmd = "lchown" + MknodCmd = "mknod" + MkfifoCmd = "mkfifo" + OpenFileCmd = "openfile" + ReadFileCmd = "readfile" + WriteFileCmd = "writefile" + ReadDirCmd = "readdir" + ResolvePathCmd = "resolvepath" + ExtractArchiveCmd = "extractarchive" + ArchivePathCmd = "archivepath" +) + +// ErrInvalid is returned if the parameters are invalid +var ErrInvalid = errors.New("invalid arguments") + +// ErrUnknown is returned for an unknown remotefs command +var ErrUnknown = errors.New("unkown command") + +// ExportedError is the serialized version of the a Go error. +// It also provides a trivial implementation of the error interface. +type ExportedError struct { + ErrString string + ErrNum int `json:",omitempty"` +} + +// Error returns an error string +func (ee *ExportedError) Error() string { + return ee.ErrString +} + +// FileInfo is the stat struct returned by the remotefs system. It +// fulfills the os.FileInfo interface. +type FileInfo struct { + NameVar string + SizeVar int64 + ModeVar os.FileMode + ModTimeVar int64 // Serialization of time.Time breaks in travis, so use an int + IsDirVar bool +} + +var _ os.FileInfo = &FileInfo{} + +// Name returns the filename from a FileInfo structure +func (f *FileInfo) Name() string { return f.NameVar } + +// Size returns the size from a FileInfo structure +func (f *FileInfo) Size() int64 { return f.SizeVar } + +// Mode returns the mode from a FileInfo structure +func (f *FileInfo) Mode() os.FileMode { return f.ModeVar } + +// ModTime returns the modification time from a FileInfo structure +func (f *FileInfo) ModTime() time.Time { return time.Unix(0, f.ModTimeVar) } + +// IsDir returns the is-directory indicator from a FileInfo structure +func (f *FileInfo) IsDir() bool { return f.IsDirVar } + +// Sys provides an interface to a FileInfo structure +func (f *FileInfo) Sys() interface{} { return nil } + +// FileHeader is a header for remote *os.File operations for remotefs.OpenFile +type FileHeader struct { + Cmd uint32 + Size uint64 +} + +const ( + // Read request command. + Read uint32 = iota + // Write request command. + Write + // Seek request command. + Seek + // Close request command. + Close + // CmdOK is a response meaning request succeeded. + CmdOK + // CmdFailed is a response meaning request failed. + CmdFailed +) + +// SeekHeader is header for the Seek operation for remotefs.OpenFile +type SeekHeader struct { + Offset int64 + Whence int32 +} diff --git a/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/remotefs.go b/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/remotefs.go new file mode 100644 index 0000000000..2d4a9f2bfe --- /dev/null +++ b/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/remotefs.go @@ -0,0 +1,546 @@ +// +build !windows + +package remotefs + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "io" + "os" + "path/filepath" + "strconv" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/symlink" + "golang.org/x/sys/unix" +) + +// Func is the function definition for a generic remote fs function +// The input to the function is any serialized structs / data from in and the string slice +// from args. The output of the function will be serialized and written to out. +type Func func(stdin io.Reader, stdout io.Writer, args []string) error + +// Commands provide a string -> remotefs function mapping. +// This is useful for commandline programs that will receive a string +// as the function to execute. +var Commands = map[string]Func{ + StatCmd: Stat, + LstatCmd: Lstat, + ReadlinkCmd: Readlink, + MkdirCmd: Mkdir, + MkdirAllCmd: MkdirAll, + RemoveCmd: Remove, + RemoveAllCmd: RemoveAll, + LinkCmd: Link, + SymlinkCmd: Symlink, + LchmodCmd: Lchmod, + LchownCmd: Lchown, + MknodCmd: Mknod, + MkfifoCmd: Mkfifo, + OpenFileCmd: OpenFile, + ReadFileCmd: ReadFile, + WriteFileCmd: WriteFile, + ReadDirCmd: ReadDir, + ResolvePathCmd: ResolvePath, + ExtractArchiveCmd: ExtractArchive, + ArchivePathCmd: ArchivePath, +} + +// Stat functions like os.Stat. +// Args: +// - args[0] is the path +// Out: +// - out = FileInfo object +func Stat(in io.Reader, out io.Writer, args []string) error { + return stat(in, out, args, os.Stat) +} + +// Lstat functions like os.Lstat. +// Args: +// - args[0] is the path +// Out: +// - out = FileInfo object +func Lstat(in io.Reader, out io.Writer, args []string) error { + return stat(in, out, args, os.Lstat) +} + +func stat(in io.Reader, out io.Writer, args []string, statfunc func(string) (os.FileInfo, error)) error { + if len(args) < 1 { + return ErrInvalid + } + + fi, err := statfunc(args[0]) + if err != nil { + return err + } + + info := FileInfo{ + NameVar: fi.Name(), + SizeVar: fi.Size(), + ModeVar: fi.Mode(), + ModTimeVar: fi.ModTime().UnixNano(), + IsDirVar: fi.IsDir(), + } + + buf, err := json.Marshal(info) + if err != nil { + return err + } + + if _, err := out.Write(buf); err != nil { + return err + } + return nil +} + +// Readlink works like os.Readlink +// In: +// - args[0] is path +// Out: +// - Write link result to out +func Readlink(in io.Reader, out io.Writer, args []string) error { + if len(args) < 1 { + return ErrInvalid + } + + l, err := os.Readlink(args[0]) + if err != nil { + return err + } + + if _, err := out.Write([]byte(l)); err != nil { + return err + } + return nil +} + +// Mkdir works like os.Mkdir +// Args: +// - args[0] is the path +// - args[1] is the permissions in octal (like 0755) +func Mkdir(in io.Reader, out io.Writer, args []string) error { + return mkdir(in, out, args, os.Mkdir) +} + +// MkdirAll works like os.MkdirAll. +// Args: +// - args[0] is the path +// - args[1] is the permissions in octal (like 0755) +func MkdirAll(in io.Reader, out io.Writer, args []string) error { + return mkdir(in, out, args, os.MkdirAll) +} + +func mkdir(in io.Reader, out io.Writer, args []string, mkdirFunc func(string, os.FileMode) error) error { + if len(args) < 2 { + return ErrInvalid + } + + perm, err := strconv.ParseUint(args[1], 8, 32) + if err != nil { + return err + } + return mkdirFunc(args[0], os.FileMode(perm)) +} + +// Remove works like os.Remove +// Args: +// - args[0] is the path +func Remove(in io.Reader, out io.Writer, args []string) error { + return remove(in, out, args, os.Remove) +} + +// RemoveAll works like os.RemoveAll +// Args: +// - args[0] is the path +func RemoveAll(in io.Reader, out io.Writer, args []string) error { + return remove(in, out, args, os.RemoveAll) +} + +func remove(in io.Reader, out io.Writer, args []string, removefunc func(string) error) error { + if len(args) < 1 { + return ErrInvalid + } + return removefunc(args[0]) +} + +// Link works like os.Link +// Args: +// - args[0] = old path name (link source) +// - args[1] = new path name (link dest) +func Link(in io.Reader, out io.Writer, args []string) error { + return link(in, out, args, os.Link) +} + +// Symlink works like os.Symlink +// Args: +// - args[0] = old path name (link source) +// - args[1] = new path name (link dest) +func Symlink(in io.Reader, out io.Writer, args []string) error { + return link(in, out, args, os.Symlink) +} + +func link(in io.Reader, out io.Writer, args []string, linkfunc func(string, string) error) error { + if len(args) < 2 { + return ErrInvalid + } + return linkfunc(args[0], args[1]) +} + +// Lchmod changes permission of the given file without following symlinks +// Args: +// - args[0] = path +// - args[1] = permission mode in octal (like 0755) +func Lchmod(in io.Reader, out io.Writer, args []string) error { + if len(args) < 2 { + return ErrInvalid + } + + perm, err := strconv.ParseUint(args[1], 8, 32) + if err != nil { + return err + } + + path := args[0] + if !filepath.IsAbs(path) { + path, err = filepath.Abs(path) + if err != nil { + return err + } + } + return unix.Fchmodat(0, path, uint32(perm), unix.AT_SYMLINK_NOFOLLOW) +} + +// Lchown works like os.Lchown +// Args: +// - args[0] = path +// - args[1] = uid in base 10 +// - args[2] = gid in base 10 +func Lchown(in io.Reader, out io.Writer, args []string) error { + if len(args) < 3 { + return ErrInvalid + } + + uid, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return err + } + + gid, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return err + } + return os.Lchown(args[0], int(uid), int(gid)) +} + +// Mknod works like syscall.Mknod +// Args: +// - args[0] = path +// - args[1] = permission mode in octal (like 0755) +// - args[2] = major device number in base 10 +// - args[3] = minor device number in base 10 +func Mknod(in io.Reader, out io.Writer, args []string) error { + if len(args) < 4 { + return ErrInvalid + } + + perm, err := strconv.ParseUint(args[1], 8, 32) + if err != nil { + return err + } + + major, err := strconv.ParseInt(args[2], 10, 32) + if err != nil { + return err + } + + minor, err := strconv.ParseInt(args[3], 10, 32) + if err != nil { + return err + } + + dev := unix.Mkdev(uint32(major), uint32(minor)) + return unix.Mknod(args[0], uint32(perm), int(dev)) +} + +// Mkfifo creates a FIFO special file with the given path name and permissions +// Args: +// - args[0] = path +// - args[1] = permission mode in octal (like 0755) +func Mkfifo(in io.Reader, out io.Writer, args []string) error { + if len(args) < 2 { + return ErrInvalid + } + + perm, err := strconv.ParseUint(args[1], 8, 32) + if err != nil { + return err + } + return unix.Mkfifo(args[0], uint32(perm)) +} + +// OpenFile works like os.OpenFile. To manage the file pointer state, +// this function acts as a single file "file server" with Read/Write/Close +// being serialized control codes from in. +// Args: +// - args[0] = path +// - args[1] = flag in base 10 +// - args[2] = permission mode in octal (like 0755) +func OpenFile(in io.Reader, out io.Writer, args []string) (err error) { + defer func() { + if err != nil { + // error code will be serialized by the caller, so don't write it here + WriteFileHeader(out, &FileHeader{Cmd: CmdFailed}, nil) + } + }() + + if len(args) < 3 { + return ErrInvalid + } + + flag, err := strconv.ParseInt(args[1], 10, 32) + if err != nil { + return err + } + + perm, err := strconv.ParseUint(args[2], 8, 32) + if err != nil { + return err + } + + f, err := os.OpenFile(args[0], int(flag), os.FileMode(perm)) + if err != nil { + return err + } + + // Signal the client that OpenFile succeeded + if err := WriteFileHeader(out, &FileHeader{Cmd: CmdOK}, nil); err != nil { + return err + } + + for { + hdr, err := ReadFileHeader(in) + if err != nil { + return err + } + + var buf []byte + switch hdr.Cmd { + case Read: + buf = make([]byte, hdr.Size, hdr.Size) + n, err := f.Read(buf) + if err != nil { + return err + } + buf = buf[:n] + case Write: + if _, err := io.CopyN(f, in, int64(hdr.Size)); err != nil { + return err + } + case Seek: + seekHdr := &SeekHeader{} + if err := binary.Read(in, binary.BigEndian, seekHdr); err != nil { + return err + } + res, err := f.Seek(seekHdr.Offset, int(seekHdr.Whence)) + if err != nil { + return err + } + buffer := &bytes.Buffer{} + if err := binary.Write(buffer, binary.BigEndian, res); err != nil { + return err + } + buf = buffer.Bytes() + case Close: + if err := f.Close(); err != nil { + return err + } + default: + return ErrUnknown + } + + retHdr := &FileHeader{ + Cmd: CmdOK, + Size: uint64(len(buf)), + } + if err := WriteFileHeader(out, retHdr, buf); err != nil { + return err + } + + if hdr.Cmd == Close { + break + } + } + return nil +} + +// ReadFile works like ioutil.ReadFile but instead writes the file to a writer +// Args: +// - args[0] = path +// Out: +// - Write file contents to out +func ReadFile(in io.Reader, out io.Writer, args []string) error { + if len(args) < 1 { + return ErrInvalid + } + + f, err := os.Open(args[0]) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(out, f); err != nil { + return nil + } + return nil +} + +// WriteFile works like ioutil.WriteFile but instead reads the file from a reader +// Args: +// - args[0] = path +// - args[1] = permission mode in octal (like 0755) +// - input data stream from in +func WriteFile(in io.Reader, out io.Writer, args []string) error { + if len(args) < 2 { + return ErrInvalid + } + + perm, err := strconv.ParseUint(args[1], 8, 32) + if err != nil { + return err + } + + f, err := os.OpenFile(args[0], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(perm)) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(f, in); err != nil { + return err + } + return nil +} + +// ReadDir works like *os.File.Readdir but instead writes the result to a writer +// Args: +// - args[0] = path +// - args[1] = number of directory entries to return. If <= 0, return all entries in directory +func ReadDir(in io.Reader, out io.Writer, args []string) error { + if len(args) < 2 { + return ErrInvalid + } + + n, err := strconv.ParseInt(args[1], 10, 32) + if err != nil { + return err + } + + f, err := os.Open(args[0]) + if err != nil { + return err + } + defer f.Close() + + infos, err := f.Readdir(int(n)) + if err != nil { + return err + } + + fileInfos := make([]FileInfo, len(infos)) + for i := range infos { + fileInfos[i] = FileInfo{ + NameVar: infos[i].Name(), + SizeVar: infos[i].Size(), + ModeVar: infos[i].Mode(), + ModTimeVar: infos[i].ModTime().UnixNano(), + IsDirVar: infos[i].IsDir(), + } + } + + buf, err := json.Marshal(fileInfos) + if err != nil { + return err + } + + if _, err := out.Write(buf); err != nil { + return err + } + return nil +} + +// ResolvePath works like docker's symlink.FollowSymlinkInScope. +// It takens in a `path` and a `root` and evaluates symlinks in `path` +// as if they were scoped in `root`. `path` must be a child path of `root`. +// In other words, `path` must have `root` as a prefix. +// Example: +// path=/foo/bar -> /baz +// root=/foo, +// Expected result = /foo/baz +// +// Args: +// - args[0] is `path` +// - args[1] is `root` +// Out: +// - Write resolved path to stdout +func ResolvePath(in io.Reader, out io.Writer, args []string) error { + if len(args) < 2 { + return ErrInvalid + } + res, err := symlink.FollowSymlinkInScope(args[0], args[1]) + if err != nil { + return err + } + if _, err = out.Write([]byte(res)); err != nil { + return err + } + return nil +} + +// ExtractArchive extracts the archive read from in. +// Args: +// - in = size of json | json of archive.TarOptions | input tar stream +// - args[0] = extract directory name +func ExtractArchive(in io.Reader, out io.Writer, args []string) error { + if len(args) < 1 { + return ErrInvalid + } + + opts, err := ReadTarOptions(in) + if err != nil { + return err + } + + if err := archive.Untar(in, args[0], opts); err != nil { + return err + } + return nil +} + +// ArchivePath archives the given directory and writes it to out. +// Args: +// - in = size of json | json of archive.TarOptions +// - args[0] = source directory name +// Out: +// - out = tar file of the archive +func ArchivePath(in io.Reader, out io.Writer, args []string) error { + if len(args) < 1 { + return ErrInvalid + } + + opts, err := ReadTarOptions(in) + if err != nil { + return err + } + + r, err := archive.TarWithOptions(args[0], opts) + if err != nil { + return err + } + + if _, err := io.Copy(out, r); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/utils.go b/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/utils.go new file mode 100644 index 0000000000..a12827c93f --- /dev/null +++ b/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/utils.go @@ -0,0 +1,168 @@ +package remotefs + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "io" + "io/ioutil" + "os" + "syscall" + + "github.com/docker/docker/pkg/archive" +) + +// ReadError is an utility function that reads a serialized error from the given reader +// and deserializes it. +func ReadError(in io.Reader) (*ExportedError, error) { + b, err := ioutil.ReadAll(in) + if err != nil { + return nil, err + } + + // No error + if len(b) == 0 { + return nil, nil + } + + var exportedErr ExportedError + if err := json.Unmarshal(b, &exportedErr); err != nil { + return nil, err + } + + return &exportedErr, nil +} + +// ExportedToError will convert a ExportedError to an error. It will try to match +// the error to any existing known error like os.ErrNotExist. Otherwise, it will just +// return an implementation of the error interface. +func ExportedToError(ee *ExportedError) error { + if ee.Error() == os.ErrNotExist.Error() { + return os.ErrNotExist + } else if ee.Error() == os.ErrExist.Error() { + return os.ErrExist + } else if ee.Error() == os.ErrPermission.Error() { + return os.ErrPermission + } + return ee +} + +// WriteError is an utility function that serializes the error +// and writes it to the output writer. +func WriteError(err error, out io.Writer) error { + if err == nil { + return nil + } + err = fixOSError(err) + + var errno int + switch typedError := err.(type) { + case *os.PathError: + if se, ok := typedError.Err.(syscall.Errno); ok { + errno = int(se) + } + case *os.LinkError: + if se, ok := typedError.Err.(syscall.Errno); ok { + errno = int(se) + } + case *os.SyscallError: + if se, ok := typedError.Err.(syscall.Errno); ok { + errno = int(se) + } + } + + exportedError := &ExportedError{ + ErrString: err.Error(), + ErrNum: errno, + } + + b, err1 := json.Marshal(exportedError) + if err1 != nil { + return err1 + } + + _, err1 = out.Write(b) + if err1 != nil { + return err1 + } + return nil +} + +// fixOSError converts possible platform dependent error into the portable errors in the +// Go os package if possible. +func fixOSError(err error) error { + // The os.IsExist, os.IsNotExist, and os.IsPermissions functions are platform + // dependent, so sending the raw error might break those functions on a different OS. + // Go defines portable errors for these. + if os.IsExist(err) { + return os.ErrExist + } else if os.IsNotExist(err) { + return os.ErrNotExist + } else if os.IsPermission(err) { + return os.ErrPermission + } + return err +} + +// ReadTarOptions reads from the specified reader and deserializes an archive.TarOptions struct. +func ReadTarOptions(r io.Reader) (*archive.TarOptions, error) { + var size uint64 + if err := binary.Read(r, binary.BigEndian, &size); err != nil { + return nil, err + } + + rawJSON := make([]byte, size) + if _, err := io.ReadFull(r, rawJSON); err != nil { + return nil, err + } + + var opts archive.TarOptions + if err := json.Unmarshal(rawJSON, &opts); err != nil { + return nil, err + } + return &opts, nil +} + +// WriteTarOptions serializes a archive.TarOptions struct and writes it to the writer. +func WriteTarOptions(w io.Writer, opts *archive.TarOptions) error { + optsBuf, err := json.Marshal(opts) + if err != nil { + return err + } + + optsSize := uint64(len(optsBuf)) + optsSizeBuf := &bytes.Buffer{} + if err := binary.Write(optsSizeBuf, binary.BigEndian, optsSize); err != nil { + return err + } + + if _, err := optsSizeBuf.WriteTo(w); err != nil { + return err + } + + if _, err := w.Write(optsBuf); err != nil { + return err + } + + return nil +} + +// ReadFileHeader reads from r and returns a deserialized FileHeader +func ReadFileHeader(r io.Reader) (*FileHeader, error) { + hdr := &FileHeader{} + if err := binary.Read(r, binary.BigEndian, hdr); err != nil { + return nil, err + } + return hdr, nil +} + +// WriteFileHeader serializes a FileHeader and writes it to w, along with any extra data +func WriteFileHeader(w io.Writer, hdr *FileHeader, extraData []byte) error { + if err := binary.Write(w, binary.BigEndian, hdr); err != nil { + return err + } + if _, err := w.Write(extraData); err != nil { + return err + } + return nil +}