From 5be7b9af3ee9b884482220979735e2a8ea969ce3 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 18 Jun 2013 20:28:49 -0700 Subject: [PATCH] * Builder: fixed the behavior of ADD to be (mostly) reverse-compatible, predictable and well-documented. --- archive.go | 83 ++++++++++++++++++++++++++++++++++-- buildfile.go | 8 ++-- docs/sources/use/builder.rst | 36 ++++++++++++++-- 3 files changed, 117 insertions(+), 10 deletions(-) diff --git a/archive.go b/archive.go index 44fdd56be3..e10fbfdae3 100644 --- a/archive.go +++ b/archive.go @@ -3,10 +3,12 @@ package docker import ( "errors" "fmt" + "github.com/dotcloud/docker/utils" "io" "io/ioutil" "os" "os/exec" + "path" ) type Archive io.Reader @@ -46,11 +48,30 @@ func (compression *Compression) Extension() string { return "" } +// Tar creates an archive from the directory at `path`, and returns it as a +// stream of bytes. func Tar(path string, compression Compression) (io.Reader, error) { - cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-c"+compression.Flag(), ".") + return TarFilter(path, compression, nil) +} + +// Tar creates an archive from the directory at `path`, only including files whose relative +// paths are included in `filter`. If `filter` is nil, then all files are included. +func TarFilter(path string, compression Compression, filter []string) (io.Reader, error) { + args := []string{"bsdtar", "-f", "-", "-C", path} + if filter == nil { + filter = []string{"."} + } + for _, f := range filter { + args = append(args, "-c"+compression.Flag(), f) + } + cmd := exec.Command(args[0], args[1:]...) return CmdStream(cmd) } +// Untar reads a stream of bytes from `archive`, parses it as a tar archive, +// and unpacks it into the directory at `path`. +// The archive may be compressed with one of the following algorithgms: +// identity (uncompressed), gzip, bzip2, xz. // FIXME: specify behavior when target path exists vs. doesn't exist. func Untar(archive io.Reader, path string) error { cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-x") @@ -65,6 +86,18 @@ func Untar(archive io.Reader, path string) error { return nil } +// 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 TarUntar(src string, filter []string, dst string) error { + utils.Debugf("TarUntar(%s %s %s)", src, filter, dst) + archive, err := TarFilter(src, Uncompressed, filter) + if err != nil { + return err + } + return Untar(archive, dst) +} + // UntarPath is a convenience function which looks for an archive // at filesystem path `src`, and unpacks it at `dst`. func UntarPath(src, dst string) error { @@ -82,11 +115,55 @@ func UntarPath(src, dst string) error { // intermediary disk IO. // func CopyWithTar(src, dst string) error { - archive, err := Tar(src, Uncompressed) + srcSt, err := os.Stat(src) if err != nil { return err } - return Untar(archive, dst) + var dstExists bool + dstSt, err := os.Stat(dst) + if err != nil { + if !os.IsNotExist(err) { + return err + } + } else { + dstExists = true + } + // Things that can go wrong if the source is a directory + if srcSt.IsDir() { + // The destination exists and is a regular file + if dstExists && !dstSt.IsDir() { + return fmt.Errorf("Can't copy a directory over a regular file") + } + // Things that can go wrong if the source is a regular file + } else { + utils.Debugf("The destination exists, it's a directory, and doesn't end in /") + // The destination exists, it's a directory, and doesn't end in / + if dstExists && dstSt.IsDir() && dst[len(dst)-1] != '/' { + return fmt.Errorf("Can't copy a regular file over a directory %s |%s|", dst, dst[len(dst)-1]) + } + } + // Create the destination + var dstDir string + if dst[len(dst)-1] == '/' { + // The destination ends in / + // --> dst is the holding directory + dstDir = dst + } else { + // The destination doesn't end in / + // --> dst is the file + dstDir = path.Dir(dst) + } + if !dstExists { + // Create the holding directory if necessary + utils.Debugf("Creating the holding directory %s", dstDir) + if err := os.MkdirAll(dstDir, 0700); err != nil && !os.IsExist(err) { + return err + } + } + if !srcSt.IsDir() { + return TarUntar(path.Dir(src), []string{path.Base(src)}, dstDir) + } + return TarUntar(src, nil, dstDir) } // CmdStream executes a command, and returns its stdout as a stream. diff --git a/buildfile.go b/buildfile.go index eb322d8176..b8ac55640e 100644 --- a/buildfile.go +++ b/buildfile.go @@ -195,15 +195,15 @@ func (b *buildFile) CmdAdd(args string) error { origPath := path.Join(b.context, orig) destPath := path.Join(container.RootfsPath(), dest) - + // Preserve the trailing '/' + if dest[len(dest)-1] == '/' { + destPath = destPath + "/" + } fi, err := os.Stat(origPath) if err != nil { return err } if fi.IsDir() { - if err := os.MkdirAll(destPath, 0700); err != nil { - return err - } if err := CopyWithTar(origPath, destPath); err != nil { return err } diff --git a/docs/sources/use/builder.rst b/docs/sources/use/builder.rst index c703fc7767..830d517c0e 100644 --- a/docs/sources/use/builder.rst +++ b/docs/sources/use/builder.rst @@ -138,9 +138,39 @@ curl was installed within the image. ``ADD `` -The `ADD` instruction will insert the files from the `` path of the context into `` path -of the container. -The context must be set in order to use this instruction. (see examples) +The `ADD` instruction will copy new files from and add them to the container's filesystem at path ``. + +`` must be the path to a file or directory relative to the source directory being built (also called the +context of the build). + +`` is the path at which the source will be copied in the destination container. + +The copy obeys the following rules: + +If `` is a directory, the entire directory is copied, including filesystem metadata. + +If `` is a tar archive in a recognized compression format (identity, gzip, bzip2 or xz), it +is unpacked as a directory. + +When a directory is copied or unpacked, it has the same behavior as 'tar -x': the result is the union of +a) whatever existed at the destination path and b) the contents of the source tree, with conflicts resolved +in favor of b on a file-by-file basis. + +If `` is any other kind of file, it is copied individually along with its metadata. + +If `` doesn't exist, it is created along with all missing directories in its path. All new +files and directories are created with mode 0700, uid and gid 0. + +If `` ends with a trailing slash '/', the contents of `` is copied `inside` it. +For example "ADD foo /usr/src/" creates /usr/src/foo in the container. If `` already exists, +it MUST be a directory. + +If `` does not end with a trailing slash '/', the contents of `` is copied `over` it. +For example "ADD foo /usr/src" creates /usr/src with the contents of the "foo". If `` already +exists, it MUST be of the same type as the source. + + + 3. Dockerfile Examples ======================