* Builder: fixed the behavior of ADD to be (mostly) reverse-compatible, predictable and well-documented.

This commit is contained in:
Solomon Hykes 2013-06-18 20:28:49 -07:00
parent edbd3da33a
commit 5be7b9af3e
3 changed files with 117 additions and 10 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -138,9 +138,39 @@ curl was installed within the image.
``ADD <src> <dest>``
The `ADD` instruction will insert the files from the `<src>` path of the context into `<dest>` 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 <src> and add them to the container's filesystem at path `<dest>`.
`<src>` must be the path to a file or directory relative to the source directory being built (also called the
context of the build).
`<dest>` is the path at which the source will be copied in the destination container.
The copy obeys the following rules:
If `<src>` is a directory, the entire directory is copied, including filesystem metadata.
If `<src>` 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 `<src>` is any other kind of file, it is copied individually along with its metadata.
If `<dest>` 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 `<dest>` ends with a trailing slash '/', the contents of `<src>` is copied `inside` it.
For example "ADD foo /usr/src/" creates /usr/src/foo in the container. If `<dest>` already exists,
it MUST be a directory.
If `<dest>` does not end with a trailing slash '/', the contents of `<src>` is copied `over` it.
For example "ADD foo /usr/src" creates /usr/src with the contents of the "foo". If `<dest>` already
exists, it MUST be of the same type as the source.
3. Dockerfile Examples
======================