summaryrefslogtreecommitdiff
path: root/vendor/github.com/containers/buildah/add.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/containers/buildah/add.go')
-rw-r--r--vendor/github.com/containers/buildah/add.go635
1 files changed, 366 insertions, 269 deletions
diff --git a/vendor/github.com/containers/buildah/add.go b/vendor/github.com/containers/buildah/add.go
index 425621028..1c1f116da 100644
--- a/vendor/github.com/containers/buildah/add.go
+++ b/vendor/github.com/containers/buildah/add.go
@@ -1,21 +1,25 @@
package buildah
import (
+ "archive/tar"
+ "fmt"
"io"
+ "io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
+ "sync"
"syscall"
"time"
+ "github.com/containers/buildah/copier"
"github.com/containers/buildah/pkg/chrootuser"
- "github.com/containers/buildah/util"
- "github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/fileutils"
"github.com/containers/storage/pkg/idtools"
+ "github.com/hashicorp/go-multierror"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -25,17 +29,22 @@ import (
type AddAndCopyOptions struct {
// Chown is a spec for the user who should be given ownership over the
// newly-added content, potentially overriding permissions which would
- // otherwise match those of local files and directories being copied.
+ // otherwise be set to 0:0.
Chown string
+ // PreserveOwnership, if Chown is not set, tells us to avoid setting
+ // ownership of copied items to 0:0, instead using whatever ownership
+ // information is already set. Not meaningful for remote sources.
+ PreserveOwnership bool
// All of the data being copied will pass through Hasher, if set.
// If the sources are URLs or files, their contents will be passed to
// Hasher.
// If the sources include directory trees, Hasher will be passed
// tar-format archives of the directory trees.
Hasher io.Writer
- // Excludes is the contents of the .dockerignore file
+ // Excludes is the contents of the .dockerignore file.
Excludes []string
- // ContextDir is the base directory for Excludes for content being copied
+ // ContextDir is the base directory for content being copied and
+ // Excludes patterns.
ContextDir string
// ID mapping options to use when contents to be copied are part of
// another container, and need ownerships to be mapped from the host to
@@ -44,74 +53,93 @@ type AddAndCopyOptions struct {
// DryRun indicates that the content should be digested, but not actually
// copied into the container.
DryRun bool
+ // Clear the setuid bit on items being copied. Has no effect on
+ // archives being extracted, where the bit is always preserved.
+ StripSetuidBit bool
+ // Clear the setgid bit on items being copied. Has no effect on
+ // archives being extracted, where the bit is always preserved.
+ StripSetgidBit bool
+ // Clear the sticky bit on items being copied. Has no effect on
+ // archives being extracted, where the bit is always preserved.
+ StripStickyBit bool
}
-// addURL copies the contents of the source URL to the destination. This is
-// its own function so that deferred closes happen after we're done pulling
-// down each item of potentially many.
-func (b *Builder) addURL(destination, srcurl string, owner idtools.IDPair, hasher io.Writer, dryRun bool) error {
- resp, err := http.Get(srcurl)
+// sourceIsRemote returns true if "source" is a remote location.
+func sourceIsRemote(source string) bool {
+ return strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://")
+}
+
+// getURL writes a tar archive containing the named content
+func getURL(src, mountpoint, renameTarget string, writer io.Writer) error {
+ url, err := url.Parse(src)
if err != nil {
- return errors.Wrapf(err, "error getting %q", srcurl)
+ return errors.Wrapf(err, "error parsing URL %q", url)
}
- defer resp.Body.Close()
-
- thisHasher := hasher
- if thisHasher != nil && b.ContentDigester.Hash() != nil {
- thisHasher = io.MultiWriter(thisHasher, b.ContentDigester.Hash())
+ response, err := http.Get(src)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing URL %q", url)
}
- if thisHasher == nil {
- thisHasher = b.ContentDigester.Hash()
+ defer response.Body.Close()
+ // Figure out what to name the new content.
+ name := renameTarget
+ if name == "" {
+ name = path.Base(url.Path)
}
- thisWriter := thisHasher
-
- if !dryRun {
- logrus.Debugf("saving %q to %q", srcurl, destination)
- f, err := os.Create(destination)
+ // If there's a date on the content, use it. If not, use the Unix epoch
+ // for compatibility.
+ date := time.Unix(0, 0).UTC()
+ lastModified := response.Header.Get("Last-Modified")
+ if lastModified != "" {
+ d, err := time.Parse(time.RFC1123, lastModified)
if err != nil {
- return errors.Wrapf(err, "error creating %q", destination)
+ return errors.Wrapf(err, "error parsing last-modified time %q", lastModified)
}
+ date = d
+ }
+ // Figure out the size of the content.
+ size := response.ContentLength
+ responseBody := response.Body
+ if size < 0 {
+ // Create a temporary file and copy the content to it, so that
+ // we can figure out how much content there is.
+ f, err := ioutil.TempFile(mountpoint, "download")
+ if err != nil {
+ return errors.Wrapf(err, "error creating temporary file to hold %q", src)
+ }
+ defer os.Remove(f.Name())
defer f.Close()
- if err = f.Chown(owner.UID, owner.GID); err != nil {
- return errors.Wrapf(err, "error setting owner of %q to %d:%d", destination, owner.UID, owner.GID)
+ size, err = io.Copy(f, response.Body)
+ if err != nil {
+ return errors.Wrapf(err, "error writing %q to temporary file %q", src, f.Name())
}
- if last := resp.Header.Get("Last-Modified"); last != "" {
- if mtime, err2 := time.Parse(time.RFC1123, last); err2 != nil {
- logrus.Debugf("error parsing Last-Modified time %q: %v", last, err2)
- } else {
- defer func() {
- if err3 := os.Chtimes(destination, time.Now(), mtime); err3 != nil {
- logrus.Debugf("error setting mtime on %q to Last-Modified time %q: %v", destination, last, err3)
- }
- }()
- }
+ _, err = f.Seek(0, io.SeekStart)
+ if err != nil {
+ return errors.Wrapf(err, "error setting up to read %q from temporary file %q", src, f.Name())
}
- defer func() {
- if err2 := f.Chmod(0600); err2 != nil {
- logrus.Debugf("error setting permissions on %q: %v", destination, err2)
- }
- }()
- thisWriter = io.MultiWriter(f, thisWriter)
+ responseBody = f
}
-
- n, err := io.Copy(thisWriter, resp.Body)
- if err != nil {
- return errors.Wrapf(err, "error reading contents for %q from %q", destination, srcurl)
+ // Write the output archive. Set permissions for compatibility.
+ tw := tar.NewWriter(writer)
+ defer tw.Close()
+ hdr := tar.Header{
+ Typeflag: tar.TypeReg,
+ Name: name,
+ Size: size,
+ Mode: 0600,
+ ModTime: date,
}
- if resp.ContentLength >= 0 && n != resp.ContentLength {
- return errors.Errorf("error reading contents for %q from %q: wrong length (%d != %d)", destination, srcurl, n, resp.ContentLength)
+ err = tw.WriteHeader(&hdr)
+ if err != nil {
+ return errors.Wrapf(err, "error writing header")
}
- return nil
+ _, err = io.Copy(tw, responseBody)
+ return errors.Wrapf(err, "error writing content from %q to tar stream", src)
}
// Add copies the contents of the specified sources into the container's root
// filesystem, optionally extracting contents of local files that look like
// non-empty archives.
-func (b *Builder) Add(destination string, extract bool, options AddAndCopyOptions, source ...string) error {
- excludes, err := dockerIgnoreMatcher(options.Excludes, options.ContextDir)
- if err != nil {
- return err
- }
+func (b *Builder) Add(destination string, extract bool, options AddAndCopyOptions, sources ...string) error {
mountPoint, err := b.Mount(b.MountLabel)
if err != nil {
return err
@@ -121,267 +149,336 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
logrus.Errorf("error unmounting container: %v", err2)
}
}()
- // Find out which user (and group) the destination should belong to.
- user, _, err := b.user(mountPoint, options.Chown)
- if err != nil {
- return err
- }
- containerOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
- hostUID, hostGID, err := util.GetHostIDs(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap, user.UID, user.GID)
- if err != nil {
- return err
+
+ contextDir := options.ContextDir
+ if contextDir == "" {
+ contextDir = string(os.PathSeparator)
}
- hostOwner := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
- dest := mountPoint
- if !options.DryRun {
- // Resolve the destination if it was specified as a relative path.
- if destination != "" && filepath.IsAbs(destination) {
- dir := filepath.Dir(destination)
- if dir != "." && dir != "/" {
- if err = idtools.MkdirAllAndChownNew(filepath.Join(dest, dir), 0755, hostOwner); err != nil {
- return errors.Wrapf(err, "error creating directory %q", filepath.Join(dest, dir))
- }
- }
- dest = filepath.Join(dest, destination)
- } else {
- if err = idtools.MkdirAllAndChownNew(filepath.Join(dest, b.WorkDir()), 0755, hostOwner); err != nil {
- return errors.Wrapf(err, "error creating directory %q", filepath.Join(dest, b.WorkDir()))
- }
- dest = filepath.Join(dest, b.WorkDir(), destination)
+
+ // Figure out what sorts of sources we have.
+ var localSources, remoteSources []string
+ for _, src := range sources {
+ if sourceIsRemote(src) {
+ remoteSources = append(remoteSources, src)
+ continue
}
- // If the destination was explicitly marked as a directory by ending it
- // with a '/', create it so that we can be sure that it's a directory,
- // and any files we're copying will be placed in the directory.
- if len(destination) > 0 && destination[len(destination)-1] == os.PathSeparator {
- if err = idtools.MkdirAllAndChownNew(dest, 0755, hostOwner); err != nil {
- return errors.Wrapf(err, "error creating directory %q", dest)
- }
+ localSources = append(localSources, src)
+ }
+
+ // Check how many items our local source specs matched. Each spec
+ // should have matched at least one item, otherwise we consider it an
+ // error.
+ var localSourceStats []*copier.StatsForGlob
+ if len(localSources) > 0 {
+ statOptions := copier.StatOptions{
+ CheckForArchives: extract,
}
- // Make sure the destination's parent directory is usable.
- if destpfi, err2 := os.Stat(filepath.Dir(dest)); err2 == nil && !destpfi.IsDir() {
- return errors.Errorf("%q already exists, but is not a subdirectory)", filepath.Dir(dest))
+ localSourceStats, err = copier.Stat(contextDir, contextDir, statOptions, localSources)
+ if err != nil {
+ return errors.Wrapf(err, "error checking on sources %v under %q", localSources, contextDir)
}
}
- // Now look at the destination itself.
- destfi, err := os.Stat(dest)
- if err != nil {
- if !os.IsNotExist(err) {
- return errors.Wrapf(err, "couldn't determine what %q is", dest)
+ numLocalSourceItems := 0
+ for _, localSourceStat := range localSourceStats {
+ if localSourceStat.Error != "" {
+ errorText := localSourceStat.Error
+ rel, err := filepath.Rel(contextDir, localSourceStat.Glob)
+ if err != nil {
+ errorText = fmt.Sprintf("%v; %s", err, errorText)
+ }
+ if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
+ errorText = fmt.Sprintf("possible escaping context directory error: %s", errorText)
+ }
+ return errors.Errorf("error checking on source %v under %q: %v", localSourceStat.Glob, contextDir, errorText)
}
- destfi = nil
- }
- if len(source) > 1 && (destfi == nil || !destfi.IsDir()) {
- return errors.Errorf("destination %q is not a directory", dest)
+ if len(localSourceStat.Globbed) == 0 {
+ return errors.Wrapf(syscall.ENOENT, "error checking on source %v under %q: no glob matches", localSourceStat.Glob, contextDir)
+ }
+ numLocalSourceItems += len(localSourceStat.Globbed)
}
- copyFileWithTar := b.copyFileWithTar(options.IDMappingOptions, &containerOwner, options.Hasher, options.DryRun)
- copyWithTar := b.copyWithTar(options.IDMappingOptions, &containerOwner, options.Hasher, options.DryRun)
- untarPath := b.untarPath(nil, options.Hasher, options.DryRun)
- err = b.addHelper(excludes, extract, dest, destfi, hostOwner, options, copyFileWithTar, copyWithTar, untarPath, source...)
- if err != nil {
- return err
+ if numLocalSourceItems+len(remoteSources) == 0 {
+ return errors.Wrapf(syscall.ENOENT, "no sources %v found", sources)
}
- return nil
-}
-// user returns the user (and group) information which the destination should belong to.
-func (b *Builder) user(mountPoint string, userspec string) (specs.User, string, error) {
- if userspec == "" {
- userspec = b.User()
+ // Find out which user (and group) the destination should belong to.
+ var chownDirs, chownFiles *idtools.IDPair
+ var chmodDirs, chmodFiles *os.FileMode
+ var user specs.User
+ if options.Chown != "" {
+ user, _, err = b.user(mountPoint, options.Chown)
+ if err != nil {
+ return errors.Wrapf(err, "error looking up UID/GID for %q", options.Chown)
+ }
+ }
+ chownDirs = &idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
+ chownFiles = &idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
+ if options.Chown == "" && options.PreserveOwnership {
+ chownDirs = nil
+ chownFiles = nil
}
- uid, gid, homeDir, err := chrootuser.GetUser(mountPoint, userspec)
- u := specs.User{
- UID: uid,
- GID: gid,
- Username: userspec,
+ // If we have a single source archive to extract, or more than one
+ // source item, or the destination has a path separator at the end of
+ // it, and it's not a remote URL, the destination needs to be a
+ // directory.
+ if destination == "" || !filepath.IsAbs(destination) {
+ tmpDestination := filepath.Join(string(os.PathSeparator)+b.WorkDir(), destination)
+ if destination == "" || strings.HasSuffix(destination, string(os.PathSeparator)) {
+ destination = tmpDestination + string(os.PathSeparator)
+ } else {
+ destination = tmpDestination
+ }
}
- if !strings.Contains(userspec, ":") {
- groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
- if err2 != nil {
- if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
- err = err2
+ destMustBeDirectory := (len(sources) > 1) || strings.HasSuffix(destination, string(os.PathSeparator))
+ destCanBeFile := false
+ if len(sources) == 1 {
+ if len(remoteSources) == 1 {
+ destCanBeFile = sourceIsRemote(sources[0])
+ }
+ if len(localSources) == 1 {
+ item := localSourceStats[0].Results[localSourceStats[0].Globbed[0]]
+ if item.IsDir || (item.IsArchive && extract) {
+ destMustBeDirectory = true
+ }
+ if item.IsRegular {
+ destCanBeFile = true
}
- } else {
- u.AdditionalGids = groups
}
-
}
- return u, homeDir, err
-}
-// dockerIgnoreMatcher returns a matcher based on the contents of the .dockerignore file under contextDir
-func dockerIgnoreMatcher(lines []string, contextDir string) (*fileutils.PatternMatcher, error) {
- // if there's no context dir, there's no .dockerignore file to consult
- if contextDir == "" {
- return nil, nil
+ // We care if the destination either doesn't exist, or exists and is a
+ // file. If the source can be a single file, for those cases we treat
+ // the destination as a file rather than as a directory tree.
+ renameTarget := ""
+ extractDirectory := filepath.Join(mountPoint, destination)
+ statOptions := copier.StatOptions{
+ CheckForArchives: extract,
}
- // If there's no .dockerignore file, then we don't have to add a
- // pattern to tell copy logic to ignore it later.
- var patterns []string
- if _, err := os.Stat(filepath.Join(contextDir, ".dockerignore")); err == nil || !os.IsNotExist(err) {
- patterns = []string{".dockerignore"}
+ destStats, err := copier.Stat(mountPoint, filepath.Join(mountPoint, b.WorkDir()), statOptions, []string{extractDirectory})
+ if err != nil {
+ return errors.Wrapf(err, "error checking on destination %v", extractDirectory)
}
- for _, ignoreSpec := range lines {
- ignoreSpec = strings.TrimSpace(ignoreSpec)
- // ignore comments passed back from .dockerignore
- if ignoreSpec == "" || ignoreSpec[0] == '#' {
- continue
- }
- // if the spec starts with '!' it means the pattern
- // should be included. make a note so that we can move
- // it to the front of the updated pattern, and insert
- // the context dir's path in between
- includeFlag := ""
- if strings.HasPrefix(ignoreSpec, "!") {
- includeFlag = "!"
- ignoreSpec = ignoreSpec[1:]
- }
- if ignoreSpec == "" {
- continue
- }
- patterns = append(patterns, includeFlag+filepath.Join(contextDir, ignoreSpec))
+ if (len(destStats) == 0 || len(destStats[0].Globbed) == 0) && !destMustBeDirectory && destCanBeFile {
+ // destination doesn't exist - extract to parent and rename the incoming file to the destination's name
+ renameTarget = filepath.Base(extractDirectory)
+ extractDirectory = filepath.Dir(extractDirectory)
}
- // if there are no patterns, save time by not constructing the object
- if len(patterns) == 0 {
- return nil, nil
+ if len(destStats) == 1 && len(destStats[0].Globbed) == 1 && destStats[0].Results[destStats[0].Globbed[0]].IsRegular {
+ if destMustBeDirectory {
+ return errors.Errorf("destination %v already exists but is not a directory", destination)
+ }
+ // destination exists - it's a file, we need to extract to parent and rename the incoming file to the destination's name
+ renameTarget = filepath.Base(extractDirectory)
+ extractDirectory = filepath.Dir(extractDirectory)
}
- // return a matcher object
- matcher, err := fileutils.NewPatternMatcher(patterns)
+
+ pm, err := fileutils.NewPatternMatcher(options.Excludes)
if err != nil {
- return nil, errors.Wrapf(err, "error creating file matcher using patterns %v", patterns)
+ return errors.Wrapf(err, "error processing excludes list %v", options.Excludes)
}
- return matcher, nil
-}
-func (b *Builder) addHelper(excludes *fileutils.PatternMatcher, extract bool, dest string, destfi os.FileInfo, hostOwner idtools.IDPair, options AddAndCopyOptions, copyFileWithTar, copyWithTar, untarPath func(src, dest string) error, source ...string) error {
- for n, src := range source {
- if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
- b.ContentDigester.Start("")
- // We assume that source is a file, and we're copying
- // it to the destination. If the destination is
- // already a directory, create a file inside of it.
- // Otherwise, the destination is the file to which
- // we'll save the contents.
- url, err := url.Parse(src)
- if err != nil {
- return errors.Wrapf(err, "error parsing URL %q", src)
+ // Copy each source in turn.
+ var srcUIDMap, srcGIDMap []idtools.IDMap
+ if options.IDMappingOptions != nil {
+ srcUIDMap, srcGIDMap = convertRuntimeIDMaps(options.IDMappingOptions.UIDMap, options.IDMappingOptions.GIDMap)
+ }
+ destUIDMap, destGIDMap := convertRuntimeIDMaps(b.IDMappingOptions.UIDMap, b.IDMappingOptions.GIDMap)
+
+ for _, src := range sources {
+ var multiErr *multierror.Error
+ var getErr, closeErr, renameErr, putErr error
+ var wg sync.WaitGroup
+ if sourceIsRemote(src) {
+ pipeReader, pipeWriter := io.Pipe()
+ wg.Add(1)
+ go func() {
+ getErr = getURL(src, mountPoint, renameTarget, pipeWriter)
+ pipeWriter.Close()
+ wg.Done()
+ }()
+ wg.Add(1)
+ go func() {
+ b.ContentDigester.Start("")
+ hashCloser := b.ContentDigester.Hash()
+ hasher := io.Writer(hashCloser)
+ if options.Hasher != nil {
+ hasher = io.MultiWriter(hasher, options.Hasher)
+ }
+ if options.DryRun {
+ _, putErr = io.Copy(hasher, pipeReader)
+ } else {
+ putOptions := copier.PutOptions{
+ UIDMap: destUIDMap,
+ GIDMap: destGIDMap,
+ ChownDirs: chownDirs,
+ ChmodDirs: chmodDirs,
+ ChownFiles: chownFiles,
+ ChmodFiles: chmodFiles,
+ }
+ putErr = copier.Put(mountPoint, extractDirectory, putOptions, io.TeeReader(pipeReader, hasher))
+ }
+ hashCloser.Close()
+ pipeReader.Close()
+ wg.Done()
+ }()
+ wg.Wait()
+ if getErr != nil {
+ getErr = errors.Wrapf(getErr, "error reading %q", src)
}
- d := dest
- if destfi != nil && destfi.IsDir() {
- d = filepath.Join(dest, path.Base(url.Path))
+ if putErr != nil {
+ putErr = errors.Wrapf(putErr, "error storing %q", src)
}
- if err = b.addURL(d, src, hostOwner, options.Hasher, options.DryRun); err != nil {
- return err
+ multiErr = multierror.Append(getErr, putErr)
+ if multiErr != nil && multiErr.ErrorOrNil() != nil {
+ if len(multiErr.Errors) > 1 {
+ return multiErr.ErrorOrNil()
+ }
+ return multiErr.Errors[0]
}
continue
}
- glob, err := filepath.Glob(src)
- if err != nil {
- return errors.Wrapf(err, "invalid glob %q", src)
+ // Dig out the result of running glob+stat on this source spec.
+ var localSourceStat *copier.StatsForGlob
+ for _, st := range localSourceStats {
+ if st.Glob == src {
+ localSourceStat = st
+ break
+ }
}
- if len(glob) == 0 {
- return errors.Wrapf(syscall.ENOENT, "no files found matching %q", src)
+ if localSourceStat == nil {
+ return errors.Errorf("internal error: should have statted %s, but we didn't?", src)
}
- for _, gsrc := range glob {
- esrc, err := filepath.EvalSymlinks(gsrc)
+ // Iterate through every item that matched the glob.
+ itemsCopied := 0
+ for _, glob := range localSourceStat.Globbed {
+ rel, err := filepath.Rel(contextDir, glob)
if err != nil {
- return errors.Wrapf(err, "error evaluating symlinks %q", gsrc)
+ return errors.Wrapf(err, "error computing path of %q", glob)
}
- srcfi, err := os.Stat(esrc)
- if err != nil {
- return errors.Wrapf(err, "error reading %q", esrc)
+ if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
+ return errors.Errorf("possible escaping context directory error: %q is outside of %q", glob, contextDir)
}
- if srcfi.IsDir() {
- b.ContentDigester.Start("dir")
- // The source is a directory, so copy the contents of
- // the source directory into the target directory. Try
- // to create it first, so that if there's a problem,
- // we'll discover why that won't work.
- if !options.DryRun {
- if err = idtools.MkdirAllAndChownNew(dest, 0755, hostOwner); err != nil {
- return errors.Wrapf(err, "error creating directory %q", dest)
- }
+ // Check for dockerignore-style exclusion of this item.
+ if rel != "." {
+ matches, err := pm.Matches(filepath.ToSlash(rel)) // nolint:staticcheck
+ if err != nil {
+ return errors.Wrapf(err, "error checking if %q(%q) is excluded", glob, rel)
}
- logrus.Debugf("copying[%d] %q to %q", n, esrc+string(os.PathSeparator)+"*", dest+string(os.PathSeparator)+"*")
-
- // Copy the whole directory because we do not exclude anything
- if excludes == nil {
- if err = copyWithTar(esrc, dest); err != nil {
- return errors.Wrapf(err, "error copying %q to %q", esrc, dest)
- }
+ if matches {
continue
}
- err := filepath.Walk(esrc, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
-
- res, err := excludes.MatchesResult(path)
- if err != nil {
- return errors.Wrapf(err, "error checking if %s is an excluded path", path)
- }
- // The latest match result has the highest priority,
- // which means that we only skip the filepath if
- // the last result matched.
- if res.IsMatched() {
- return nil
- }
-
- // combine the source's basename with the dest directory
- fpath, err := filepath.Rel(esrc, path)
- if err != nil {
- return errors.Wrapf(err, "error converting %s to a path relative to %s", path, esrc)
- }
- if err = copyFileWithTar(path, filepath.Join(dest, fpath)); err != nil {
- return errors.Wrapf(err, "error copying %q to %q", path, dest)
- }
- return nil
- })
- if err != nil {
- return err
- }
- continue
}
-
- // This source is a file
- // Check if the path matches the .dockerignore
- if excludes != nil {
- res, err := excludes.MatchesResult(esrc)
- if err != nil {
- return errors.Wrapf(err, "error checking if %s is an excluded path", esrc)
+ st := localSourceStat.Results[glob]
+ pipeReader, pipeWriter := io.Pipe()
+ wg.Add(1)
+ go func() {
+ renamedItems := 0
+ writer := io.WriteCloser(pipeWriter)
+ if renameTarget != "" {
+ writer = newTarFilterer(writer, func(hdr *tar.Header) (bool, bool, io.Reader) {
+ hdr.Name = renameTarget
+ renamedItems++
+ return false, false, nil
+ })
}
- // Skip the file if the pattern matches
- if res.IsMatched() {
- continue
+ getOptions := copier.GetOptions{
+ UIDMap: srcUIDMap,
+ GIDMap: srcGIDMap,
+ Excludes: options.Excludes,
+ ExpandArchives: extract,
+ StripSetuidBit: options.StripSetuidBit,
+ StripSetgidBit: options.StripSetgidBit,
+ StripStickyBit: options.StripStickyBit,
}
- }
-
- b.ContentDigester.Start("file")
-
- if !extract || !archive.IsArchivePath(esrc) {
- // This source is a file, and either it's not an
- // archive, or we don't care whether or not it's an
- // archive.
- d := dest
- if destfi != nil && destfi.IsDir() {
- d = filepath.Join(dest, filepath.Base(gsrc))
+ getErr = copier.Get(contextDir, contextDir, getOptions, []string{glob}, writer)
+ closeErr = writer.Close()
+ if renameTarget != "" && renamedItems > 1 {
+ renameErr = errors.Errorf("internal error: renamed %d items when we expected to only rename 1", renamedItems)
+ }
+ wg.Done()
+ }()
+ wg.Add(1)
+ go func() {
+ if st.IsDir {
+ b.ContentDigester.Start("dir")
+ } else {
+ b.ContentDigester.Start("file")
}
- // Copy the file, preserving attributes.
- logrus.Debugf("copying[%d] %q to %q", n, esrc, d)
- if err = copyFileWithTar(esrc, d); err != nil {
- return errors.Wrapf(err, "error copying %q to %q", esrc, d)
+ hashCloser := b.ContentDigester.Hash()
+ hasher := io.Writer(hashCloser)
+ if options.Hasher != nil {
+ hasher = io.MultiWriter(hasher, options.Hasher)
+ }
+ if options.DryRun {
+ _, putErr = io.Copy(hasher, pipeReader)
+ } else {
+ putOptions := copier.PutOptions{
+ UIDMap: destUIDMap,
+ GIDMap: destGIDMap,
+ ChownDirs: chownDirs,
+ ChmodDirs: chmodDirs,
+ ChownFiles: chownFiles,
+ ChmodFiles: chmodFiles,
+ }
+ putErr = copier.Put(mountPoint, extractDirectory, putOptions, io.TeeReader(pipeReader, hasher))
}
- continue
+ hashCloser.Close()
+ pipeReader.Close()
+ wg.Done()
+ }()
+ wg.Wait()
+ if getErr != nil {
+ getErr = errors.Wrapf(getErr, "error reading %q", src)
}
-
- // We're extracting an archive into the destination directory.
- logrus.Debugf("extracting contents[%d] of %q into %q", n, esrc, dest)
- if err = untarPath(esrc, dest); err != nil {
- return errors.Wrapf(err, "error extracting %q into %q", esrc, dest)
+ if closeErr != nil {
+ closeErr = errors.Wrapf(closeErr, "error closing %q", src)
+ }
+ if renameErr != nil {
+ renameErr = errors.Wrapf(renameErr, "error renaming %q", src)
+ }
+ if putErr != nil {
+ putErr = errors.Wrapf(putErr, "error storing %q", src)
+ }
+ multiErr = multierror.Append(getErr, closeErr, renameErr, putErr)
+ if multiErr != nil && multiErr.ErrorOrNil() != nil {
+ if len(multiErr.Errors) > 1 {
+ return multiErr.ErrorOrNil()
+ }
+ return multiErr.Errors[0]
}
+ itemsCopied++
+ }
+ if itemsCopied == 0 {
+ return errors.Wrapf(syscall.ENOENT, "no items matching glob %q copied (%d filtered)", localSourceStat.Glob, len(localSourceStat.Globbed))
}
}
return nil
}
+
+// user returns the user (and group) information which the destination should belong to.
+func (b *Builder) user(mountPoint string, userspec string) (specs.User, string, error) {
+ if userspec == "" {
+ userspec = b.User()
+ }
+
+ uid, gid, homeDir, err := chrootuser.GetUser(mountPoint, userspec)
+ u := specs.User{
+ UID: uid,
+ GID: gid,
+ Username: userspec,
+ }
+ if !strings.Contains(userspec, ":") {
+ groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
+ if err2 != nil {
+ if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
+ err = err2
+ }
+ } else {
+ u.AdditionalGids = groups
+ }
+
+ }
+ return u, homeDir, err
+}