Add support for copy/add with multiple src files

Part one of solution for issue #6820

Signed-off-by: Doug Davis <dug@us.ibm.com>
This commit is contained in:
Doug Davis 2014-09-16 09:58:20 -07:00
parent 72e9015591
commit 05b8a1eb36
14 changed files with 253 additions and 77 deletions

View file

@ -65,8 +65,8 @@ func maintainer(b *Builder, args []string, attributes map[string]bool) error {
// exist here. If you do not wish to have this automatic handling, use COPY.
//
func add(b *Builder, args []string, attributes map[string]bool) error {
if len(args) != 2 {
return fmt.Errorf("ADD requires two arguments")
if len(args) < 2 {
return fmt.Errorf("ADD requires at least two arguments")
}
return b.runContextCommand(args, true, true, "ADD")
@ -77,8 +77,8 @@ func add(b *Builder, args []string, attributes map[string]bool) error {
// Same as 'ADD' but without the tar and remote url handling.
//
func dispatchCopy(b *Builder, args []string, attributes map[string]bool) error {
if len(args) != 2 {
return fmt.Errorf("COPY requires two arguments")
if len(args) < 2 {
return fmt.Errorf("COPY requires at least two arguments")
}
return b.runContextCommand(args, false, false, "COPY")

View file

@ -99,37 +99,117 @@ func (b *Builder) commit(id string, autoCmd []string, comment string) error {
return nil
}
type copyInfo struct {
origPath string
destPath string
hashPath string
decompress bool
tmpDir string
}
func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecompression bool, cmdName string) error {
if b.context == nil {
return fmt.Errorf("No context given. Impossible to use %s", cmdName)
}
if len(args) != 2 {
return fmt.Errorf("Invalid %s format", cmdName)
if len(args) < 2 {
return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName)
}
orig := args[0]
dest := args[1]
dest := args[len(args)-1] // last one is always the dest
if len(args) > 2 && dest[len(dest)-1] != '/' {
return fmt.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName)
}
copyInfos := make([]copyInfo, len(args)-1)
hasHash := false
srcPaths := ""
origPaths := ""
cmd := b.Config.Cmd
b.Config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, orig, dest)}
defer func(cmd []string) { b.Config.Cmd = cmd }(cmd)
b.Config.Image = b.image
defer func() {
for _, ci := range copyInfos {
if ci.tmpDir != "" {
os.RemoveAll(ci.tmpDir)
}
}
}()
// Loop through each src file and calculate the info we need to
// do the copy (e.g. hash value if cached). Don't actually do
// the copy until we've looked at all src files
for i, orig := range args[0 : len(args)-1] {
ci := &copyInfos[i]
ci.origPath = orig
ci.destPath = dest
ci.decompress = true
err := calcCopyInfo(b, cmdName, ci, allowRemote, allowDecompression)
if err != nil {
return err
}
origPaths += " " + ci.origPath // will have leading space
if ci.hashPath == "" {
srcPaths += " " + ci.origPath // note leading space
} else {
srcPaths += " " + ci.hashPath // note leading space
hasHash = true
}
}
cmd := b.Config.Cmd
b.Config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s%s in %s", cmdName, srcPaths, dest)}
defer func(cmd []string) { b.Config.Cmd = cmd }(cmd)
hit, err := b.probeCache()
if err != nil {
return err
}
// If we do not have at least one hash, never use the cache
if hit && hasHash {
return nil
}
container, _, err := b.Daemon.Create(b.Config, "")
if err != nil {
return err
}
b.TmpContainers[container.ID] = struct{}{}
if err := container.Mount(); err != nil {
return err
}
defer container.Unmount()
for _, ci := range copyInfos {
if err := b.addContext(container, ci.origPath, ci.destPath, ci.decompress); err != nil {
return err
}
}
if err := b.commit(container.ID, cmd, fmt.Sprintf("%s%s in %s", cmdName, origPaths, dest)); err != nil {
return err
}
return nil
}
func calcCopyInfo(b *Builder, cmdName string, ci *copyInfo, allowRemote bool, allowDecompression bool) error {
var (
origPath = orig
destPath = dest
remoteHash string
isRemote bool
decompress = true
)
isRemote = utils.IsURL(orig)
saveOrig := ci.origPath
isRemote = utils.IsURL(ci.origPath)
if isRemote && !allowRemote {
return fmt.Errorf("Source can't be an URL for %s", cmdName)
} else if utils.IsURL(orig) {
} else if isRemote {
// Initiate the download
resp, err := utils.Download(orig)
resp, err := utils.Download(ci.origPath)
if err != nil {
return err
}
@ -139,6 +219,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp
if err != nil {
return err
}
ci.tmpDir = tmpDirName
// Create a tmp file within our tmp dir
tmpFileName := path.Join(tmpDirName, "tmp")
@ -146,7 +227,6 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp
if err != nil {
return err
}
defer os.RemoveAll(tmpDirName)
// Download and dump result to tmp file
if _, err := io.Copy(tmpFile, utils.ProgressReader(resp.Body, int(resp.ContentLength), b.OutOld, b.StreamFormatter, true, "", "Downloading")); err != nil {
@ -161,7 +241,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp
return err
}
origPath = path.Join(filepath.Base(tmpDirName), filepath.Base(tmpFileName))
ci.origPath = path.Join(filepath.Base(tmpDirName), filepath.Base(tmpFileName))
// Process the checksum
r, err := archive.Tar(tmpFileName, archive.Uncompressed)
@ -179,8 +259,8 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp
r.Close()
// If the destination is a directory, figure out the filename.
if strings.HasSuffix(dest, "/") {
u, err := url.Parse(orig)
if strings.HasSuffix(ci.destPath, "/") {
u, err := url.Parse(saveOrig)
if err != nil {
return err
}
@ -193,30 +273,29 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp
if filename == "" {
return fmt.Errorf("cannot determine filename from url: %s", u)
}
destPath = dest + filename
ci.destPath = ci.destPath + filename
}
}
if err := b.checkPathForAddition(origPath); err != nil {
if err := b.checkPathForAddition(ci.origPath); err != nil {
return err
}
// Hash path and check the cache
if b.UtilizeCache {
var (
hash string
sums = b.context.GetSums()
)
if remoteHash != "" {
hash = remoteHash
} else if fi, err := os.Stat(path.Join(b.contextPath, origPath)); err != nil {
ci.hashPath = remoteHash
} else if fi, err := os.Stat(path.Join(b.contextPath, ci.origPath)); err != nil {
return err
} else if fi.IsDir() {
var subfiles []string
for _, fileInfo := range sums {
absFile := path.Join(b.contextPath, fileInfo.Name())
absOrigPath := path.Join(b.contextPath, origPath)
absOrigPath := path.Join(b.contextPath, ci.origPath)
if strings.HasPrefix(absFile, absOrigPath) {
subfiles = append(subfiles, fileInfo.Sum())
}
@ -224,49 +303,22 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp
sort.Strings(subfiles)
hasher := sha256.New()
hasher.Write([]byte(strings.Join(subfiles, ",")))
hash = "dir:" + hex.EncodeToString(hasher.Sum(nil))
ci.hashPath = "dir:" + hex.EncodeToString(hasher.Sum(nil))
} else {
if origPath[0] == '/' && len(origPath) > 1 {
origPath = origPath[1:]
if ci.origPath[0] == '/' && len(ci.origPath) > 1 {
ci.origPath = ci.origPath[1:]
}
origPath = strings.TrimPrefix(origPath, "./")
ci.origPath = strings.TrimPrefix(ci.origPath, "./")
// This will match on the first file in sums of the archive
if fis := sums.GetFile(origPath); fis != nil {
hash = "file:" + fis.Sum()
if fis := sums.GetFile(ci.origPath); fis != nil {
ci.hashPath = "file:" + fis.Sum()
}
}
b.Config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, hash, dest)}
hit, err := b.probeCache()
if err != nil {
return err
}
// If we do not have a hash, never use the cache
if hit && hash != "" {
return nil
}
}
// Create the container
container, _, err := b.Daemon.Create(b.Config, "")
if err != nil {
return err
}
b.TmpContainers[container.ID] = struct{}{}
if err := container.Mount(); err != nil {
return err
}
defer container.Unmount()
if !allowDecompression || isRemote {
decompress = false
}
if err := b.addContext(container, origPath, destPath, decompress); err != nil {
return err
}
if err := b.commit(container.ID, cmd, fmt.Sprintf("%s %s in %s", cmdName, orig, dest)); err != nil {
return err
ci.decompress = false
}
return nil
}

View file

@ -131,12 +131,13 @@ or
interactively, as with the following command: **docker run -t -i image bash**
**ADD**
--**ADD <src> <dest>** The ADD instruction copies new files from <src> and adds them
to the filesystem of the container at path <dest>. <src> must be the path to a
file or directory relative to the source directory that is being built (the
context of the build) or a remote file URL. <dest> is the absolute path to
which the source is copied inside the target container. All new files and
directories are created with mode 0755, with uid and gid 0.
--**ADD <src>... <dest>** The ADD instruction copies new files, directories
or remote file URLs to the filesystem of the container at path <dest>.
Mutliple <src> resources may be specified but if they are files or directories
then they must be relative to the source directory that is being built
(the context of the build). <dest> is the absolute path to
which the source is copied inside the target container. All new files and
directories are created with mode 0755, with uid and gid 0.
**ENTRYPOINT**
--**ENTRYPOINT** has two forms: ENTRYPOINT ["executable", "param1", "param2"]

View file

@ -284,13 +284,15 @@ change them using `docker run --env <key>=<value>`.
## ADD
ADD <src> <dest>
ADD <src>... <dest>
The `ADD` instruction will copy new files from `<src>` and add them to the
container's filesystem at path `<dest>`.
The `ADD` instruction copies new files,directories or remote file URLs to
the filesystem of the container from `<src>` and add them to the 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) or a remote file URL.
Multiple <src> resource may be specified but if they are files or
directories then they must be relative to the source directory that is
being built (the context of the build).
`<dest>` is the absolute path to which the source will be copied inside the
destination container.
@ -353,6 +355,9 @@ The copy obeys the following rules:
will be considered a directory and the contents of `<src>` will be written
at `<dest>/base(<src>)`.
- If multiple `<src>` resources are specified then `<dest>` must be a
directory, and it must end with a slash `/`.
- If `<dest>` does not end with a trailing slash, it will be considered a
regular file and the contents of `<src>` will be written at `<dest>`.
@ -361,13 +366,15 @@ The copy obeys the following rules:
## COPY
COPY <src> <dest>
COPY <src>... <dest>
The `COPY` instruction will copy new files from `<src>` and add them to the
container's filesystem at path `<dest>`.
The `COPY` instruction copies new files,directories or remote file URLs to
the filesystem of the container from `<src>` and add them to the 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).
Multiple <src> resource may be specified but if they are files or
directories then they must be relative to the source directory that is being
built (the context of the build).
`<dest>` is the absolute path to which the source will be copied inside the
destination container.
@ -393,6 +400,9 @@ The copy obeys the following rules:
will be considered a directory and the contents of `<src>` will be written
at `<dest>/base(<src>)`.
- If multiple `<src>` resources are specified then `<dest>` must be a
directory, and it must end with a slash `/`.
- If `<dest>` does not end with a trailing slash, it will be considered a
regular file and the contents of `<src>` will be written at `<dest>`.

View file

@ -0,0 +1,17 @@
FROM busybox
RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
RUN echo 'dockerio:x:1001:' >> /etc/group
RUN mkdir /exists
RUN touch /exists/exists_file
RUN chown -R dockerio.dockerio /exists
COPY test_file1 test_file2 /exists/
ADD test_file3 test_file4 https://docker.com/robots.txt /exists/
RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
RUN [ $(ls -l /exists/test_file1 | awk '{print $3":"$4}') = 'root:root' ]
RUN [ $(ls -l /exists/test_file2 | awk '{print $3":"$4}') = 'root:root' ]
RUN [ $(ls -l /exists/test_file3 | awk '{print $3":"$4}') = 'root:root' ]
RUN [ $(ls -l /exists/test_file4 | awk '{print $3":"$4}') = 'root:root' ]
RUN [ $(ls -l /exists/robots.txt | awk '{print $3":"$4}') = 'root:root' ]
RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ]

View file

@ -0,0 +1,7 @@
FROM busybox
RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
RUN echo 'dockerio:x:1001:' >> /etc/group
RUN mkdir /exists
RUN chown -R dockerio.dockerio /exists
COPY test_file1 /exists/
ADD test_file2 test_file3 /exists/test_file1

View file

@ -148,6 +148,66 @@ func TestAddSingleFileToExistDir(t *testing.T) {
logDone("build - add single file to existing dir")
}
func TestMultipleFiles(t *testing.T) {
buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
out, exitCode, err := dockerCmdInDir(t, buildDirectory, "build", "-t", "testaddimg", "MultipleFiles")
errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
if err != nil || exitCode != 0 {
t.Fatal("failed to build the image")
}
deleteImages("testaddimg")
logDone("build - mulitple file copy/add tests")
}
func TestAddMultipleFilesToFile(t *testing.T) {
name := "testaddmultiplefilestofile"
defer deleteImages(name)
ctx, err := fakeContext(`FROM scratch
ADD file1.txt file2.txt test
`,
map[string]string{
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
t.Fatal(err)
}
expected := "When using ADD with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err)
}
logDone("build - multiple add files to file")
}
func TestCopyMultipleFilesToFile(t *testing.T) {
name := "testcopymultiplefilestofile"
defer deleteImages(name)
ctx, err := fakeContext(`FROM scratch
COPY file1.txt file2.txt test
`,
map[string]string{
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
t.Fatal(err)
}
expected := "When using COPY with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err)
}
logDone("build - multiple copy files to file")
}
func TestAddSingleFileToNonExistDir(t *testing.T) {
buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestAdd")
out, exitCode, err := dockerCmdInDir(t, buildDirectory, "build", "-t", "testaddimg", "SingleFileToNonExistDir")
@ -1059,6 +1119,35 @@ func TestBuildADDLocalFileWithCache(t *testing.T) {
logDone("build - add local file with cache")
}
func TestBuildADDMultipleLocalFileWithCache(t *testing.T) {
name := "testbuildaddmultiplelocalfilewithcache"
defer deleteImages(name)
dockerfile := `
FROM busybox
MAINTAINER dockerio
ADD foo Dockerfile /usr/lib/bla/
RUN [ "$(cat /usr/lib/bla/foo)" = "hello" ]`
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
t.Fatal(err)
}
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
t.Fatal(err)
}
id2, err := buildImageFromContext(name, ctx, true)
if err != nil {
t.Fatal(err)
}
if id1 != id2 {
t.Fatal("The cache should have been used but hasn't.")
}
logDone("build - add multiple local files with cache")
}
func TestBuildADDLocalFileWithoutCache(t *testing.T) {
name := "testbuildaddlocalfilewithoutcache"
defer deleteImages(name)