Part one of solution for issue #6820
Signed-off-by: Doug Davis <dug@us.ibm.com>
| ... | ... |
@@ -65,8 +65,8 @@ func maintainer(b *Builder, args []string, attributes map[string]bool) error {
|
| 65 | 65 |
// exist here. If you do not wish to have this automatic handling, use COPY. |
| 66 | 66 |
// |
| 67 | 67 |
func add(b *Builder, args []string, attributes map[string]bool) error {
|
| 68 |
- if len(args) != 2 {
|
|
| 69 |
- return fmt.Errorf("ADD requires two arguments")
|
|
| 68 |
+ if len(args) < 2 {
|
|
| 69 |
+ return fmt.Errorf("ADD requires at least two arguments")
|
|
| 70 | 70 |
} |
| 71 | 71 |
|
| 72 | 72 |
return b.runContextCommand(args, true, true, "ADD") |
| ... | ... |
@@ -77,8 +77,8 @@ func add(b *Builder, args []string, attributes map[string]bool) error {
|
| 77 | 77 |
// Same as 'ADD' but without the tar and remote url handling. |
| 78 | 78 |
// |
| 79 | 79 |
func dispatchCopy(b *Builder, args []string, attributes map[string]bool) error {
|
| 80 |
- if len(args) != 2 {
|
|
| 81 |
- return fmt.Errorf("COPY requires two arguments")
|
|
| 80 |
+ if len(args) < 2 {
|
|
| 81 |
+ return fmt.Errorf("COPY requires at least two arguments")
|
|
| 82 | 82 |
} |
| 83 | 83 |
|
| 84 | 84 |
return b.runContextCommand(args, false, false, "COPY") |
| ... | ... |
@@ -99,37 +99,117 @@ func (b *Builder) commit(id string, autoCmd []string, comment string) error {
|
| 99 | 99 |
return nil |
| 100 | 100 |
} |
| 101 | 101 |
|
| 102 |
+type copyInfo struct {
|
|
| 103 |
+ origPath string |
|
| 104 |
+ destPath string |
|
| 105 |
+ hashPath string |
|
| 106 |
+ decompress bool |
|
| 107 |
+ tmpDir string |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 102 | 110 |
func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecompression bool, cmdName string) error {
|
| 103 | 111 |
if b.context == nil {
|
| 104 | 112 |
return fmt.Errorf("No context given. Impossible to use %s", cmdName)
|
| 105 | 113 |
} |
| 106 | 114 |
|
| 107 |
- if len(args) != 2 {
|
|
| 108 |
- return fmt.Errorf("Invalid %s format", cmdName)
|
|
| 115 |
+ if len(args) < 2 {
|
|
| 116 |
+ return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName)
|
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ dest := args[len(args)-1] // last one is always the dest |
|
| 120 |
+ |
|
| 121 |
+ if len(args) > 2 && dest[len(dest)-1] != '/' {
|
|
| 122 |
+ return fmt.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName)
|
|
| 109 | 123 |
} |
| 110 | 124 |
|
| 111 |
- orig := args[0] |
|
| 112 |
- dest := args[1] |
|
| 125 |
+ copyInfos := make([]copyInfo, len(args)-1) |
|
| 126 |
+ hasHash := false |
|
| 127 |
+ srcPaths := "" |
|
| 128 |
+ origPaths := "" |
|
| 129 |
+ |
|
| 130 |
+ b.Config.Image = b.image |
|
| 131 |
+ |
|
| 132 |
+ defer func() {
|
|
| 133 |
+ for _, ci := range copyInfos {
|
|
| 134 |
+ if ci.tmpDir != "" {
|
|
| 135 |
+ os.RemoveAll(ci.tmpDir) |
|
| 136 |
+ } |
|
| 137 |
+ } |
|
| 138 |
+ }() |
|
| 139 |
+ |
|
| 140 |
+ // Loop through each src file and calculate the info we need to |
|
| 141 |
+ // do the copy (e.g. hash value if cached). Don't actually do |
|
| 142 |
+ // the copy until we've looked at all src files |
|
| 143 |
+ for i, orig := range args[0 : len(args)-1] {
|
|
| 144 |
+ ci := ©Infos[i] |
|
| 145 |
+ ci.origPath = orig |
|
| 146 |
+ ci.destPath = dest |
|
| 147 |
+ ci.decompress = true |
|
| 148 |
+ |
|
| 149 |
+ err := calcCopyInfo(b, cmdName, ci, allowRemote, allowDecompression) |
|
| 150 |
+ if err != nil {
|
|
| 151 |
+ return err |
|
| 152 |
+ } |
|
| 153 |
+ |
|
| 154 |
+ origPaths += " " + ci.origPath // will have leading space |
|
| 155 |
+ if ci.hashPath == "" {
|
|
| 156 |
+ srcPaths += " " + ci.origPath // note leading space |
|
| 157 |
+ } else {
|
|
| 158 |
+ srcPaths += " " + ci.hashPath // note leading space |
|
| 159 |
+ hasHash = true |
|
| 160 |
+ } |
|
| 161 |
+ } |
|
| 113 | 162 |
|
| 114 | 163 |
cmd := b.Config.Cmd |
| 115 |
- b.Config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, orig, dest)}
|
|
| 164 |
+ b.Config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s%s in %s", cmdName, srcPaths, dest)}
|
|
| 116 | 165 |
defer func(cmd []string) { b.Config.Cmd = cmd }(cmd)
|
| 117 |
- b.Config.Image = b.image |
|
| 118 | 166 |
|
| 167 |
+ hit, err := b.probeCache() |
|
| 168 |
+ if err != nil {
|
|
| 169 |
+ return err |
|
| 170 |
+ } |
|
| 171 |
+ // If we do not have at least one hash, never use the cache |
|
| 172 |
+ if hit && hasHash {
|
|
| 173 |
+ return nil |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ container, _, err := b.Daemon.Create(b.Config, "") |
|
| 177 |
+ if err != nil {
|
|
| 178 |
+ return err |
|
| 179 |
+ } |
|
| 180 |
+ b.TmpContainers[container.ID] = struct{}{}
|
|
| 181 |
+ |
|
| 182 |
+ if err := container.Mount(); err != nil {
|
|
| 183 |
+ return err |
|
| 184 |
+ } |
|
| 185 |
+ defer container.Unmount() |
|
| 186 |
+ |
|
| 187 |
+ for _, ci := range copyInfos {
|
|
| 188 |
+ if err := b.addContext(container, ci.origPath, ci.destPath, ci.decompress); err != nil {
|
|
| 189 |
+ return err |
|
| 190 |
+ } |
|
| 191 |
+ } |
|
| 192 |
+ |
|
| 193 |
+ if err := b.commit(container.ID, cmd, fmt.Sprintf("%s%s in %s", cmdName, origPaths, dest)); err != nil {
|
|
| 194 |
+ return err |
|
| 195 |
+ } |
|
| 196 |
+ return nil |
|
| 197 |
+} |
|
| 198 |
+ |
|
| 199 |
+func calcCopyInfo(b *Builder, cmdName string, ci *copyInfo, allowRemote bool, allowDecompression bool) error {
|
|
| 119 | 200 |
var ( |
| 120 |
- origPath = orig |
|
| 121 |
- destPath = dest |
|
| 122 | 201 |
remoteHash string |
| 123 | 202 |
isRemote bool |
| 124 |
- decompress = true |
|
| 125 | 203 |
) |
| 126 | 204 |
|
| 127 |
- isRemote = utils.IsURL(orig) |
|
| 205 |
+ saveOrig := ci.origPath |
|
| 206 |
+ isRemote = utils.IsURL(ci.origPath) |
|
| 207 |
+ |
|
| 128 | 208 |
if isRemote && !allowRemote {
|
| 129 | 209 |
return fmt.Errorf("Source can't be an URL for %s", cmdName)
|
| 130 |
- } else if utils.IsURL(orig) {
|
|
| 210 |
+ } else if isRemote {
|
|
| 131 | 211 |
// Initiate the download |
| 132 |
- resp, err := utils.Download(orig) |
|
| 212 |
+ resp, err := utils.Download(ci.origPath) |
|
| 133 | 213 |
if err != nil {
|
| 134 | 214 |
return err |
| 135 | 215 |
} |
| ... | ... |
@@ -139,6 +219,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp |
| 139 | 139 |
if err != nil {
|
| 140 | 140 |
return err |
| 141 | 141 |
} |
| 142 |
+ ci.tmpDir = tmpDirName |
|
| 142 | 143 |
|
| 143 | 144 |
// Create a tmp file within our tmp dir |
| 144 | 145 |
tmpFileName := path.Join(tmpDirName, "tmp") |
| ... | ... |
@@ -146,7 +227,6 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp |
| 146 | 146 |
if err != nil {
|
| 147 | 147 |
return err |
| 148 | 148 |
} |
| 149 |
- defer os.RemoveAll(tmpDirName) |
|
| 150 | 149 |
|
| 151 | 150 |
// Download and dump result to tmp file |
| 152 | 151 |
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 |
| 161 | 161 |
return err |
| 162 | 162 |
} |
| 163 | 163 |
|
| 164 |
- origPath = path.Join(filepath.Base(tmpDirName), filepath.Base(tmpFileName)) |
|
| 164 |
+ ci.origPath = path.Join(filepath.Base(tmpDirName), filepath.Base(tmpFileName)) |
|
| 165 | 165 |
|
| 166 | 166 |
// Process the checksum |
| 167 | 167 |
r, err := archive.Tar(tmpFileName, archive.Uncompressed) |
| ... | ... |
@@ -179,8 +259,8 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp |
| 179 | 179 |
r.Close() |
| 180 | 180 |
|
| 181 | 181 |
// If the destination is a directory, figure out the filename. |
| 182 |
- if strings.HasSuffix(dest, "/") {
|
|
| 183 |
- u, err := url.Parse(orig) |
|
| 182 |
+ if strings.HasSuffix(ci.destPath, "/") {
|
|
| 183 |
+ u, err := url.Parse(saveOrig) |
|
| 184 | 184 |
if err != nil {
|
| 185 | 185 |
return err |
| 186 | 186 |
} |
| ... | ... |
@@ -193,30 +273,29 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp |
| 193 | 193 |
if filename == "" {
|
| 194 | 194 |
return fmt.Errorf("cannot determine filename from url: %s", u)
|
| 195 | 195 |
} |
| 196 |
- destPath = dest + filename |
|
| 196 |
+ ci.destPath = ci.destPath + filename |
|
| 197 | 197 |
} |
| 198 | 198 |
} |
| 199 | 199 |
|
| 200 |
- if err := b.checkPathForAddition(origPath); err != nil {
|
|
| 200 |
+ if err := b.checkPathForAddition(ci.origPath); err != nil {
|
|
| 201 | 201 |
return err |
| 202 | 202 |
} |
| 203 | 203 |
|
| 204 | 204 |
// Hash path and check the cache |
| 205 | 205 |
if b.UtilizeCache {
|
| 206 | 206 |
var ( |
| 207 |
- hash string |
|
| 208 | 207 |
sums = b.context.GetSums() |
| 209 | 208 |
) |
| 210 | 209 |
|
| 211 | 210 |
if remoteHash != "" {
|
| 212 |
- hash = remoteHash |
|
| 213 |
- } else if fi, err := os.Stat(path.Join(b.contextPath, origPath)); err != nil {
|
|
| 211 |
+ ci.hashPath = remoteHash |
|
| 212 |
+ } else if fi, err := os.Stat(path.Join(b.contextPath, ci.origPath)); err != nil {
|
|
| 214 | 213 |
return err |
| 215 | 214 |
} else if fi.IsDir() {
|
| 216 | 215 |
var subfiles []string |
| 217 | 216 |
for _, fileInfo := range sums {
|
| 218 | 217 |
absFile := path.Join(b.contextPath, fileInfo.Name()) |
| 219 |
- absOrigPath := path.Join(b.contextPath, origPath) |
|
| 218 |
+ absOrigPath := path.Join(b.contextPath, ci.origPath) |
|
| 220 | 219 |
if strings.HasPrefix(absFile, absOrigPath) {
|
| 221 | 220 |
subfiles = append(subfiles, fileInfo.Sum()) |
| 222 | 221 |
} |
| ... | ... |
@@ -224,49 +303,22 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecomp |
| 224 | 224 |
sort.Strings(subfiles) |
| 225 | 225 |
hasher := sha256.New() |
| 226 | 226 |
hasher.Write([]byte(strings.Join(subfiles, ","))) |
| 227 |
- hash = "dir:" + hex.EncodeToString(hasher.Sum(nil)) |
|
| 227 |
+ ci.hashPath = "dir:" + hex.EncodeToString(hasher.Sum(nil)) |
|
| 228 | 228 |
} else {
|
| 229 |
- if origPath[0] == '/' && len(origPath) > 1 {
|
|
| 230 |
- origPath = origPath[1:] |
|
| 229 |
+ if ci.origPath[0] == '/' && len(ci.origPath) > 1 {
|
|
| 230 |
+ ci.origPath = ci.origPath[1:] |
|
| 231 | 231 |
} |
| 232 |
- origPath = strings.TrimPrefix(origPath, "./") |
|
| 232 |
+ ci.origPath = strings.TrimPrefix(ci.origPath, "./") |
|
| 233 | 233 |
// This will match on the first file in sums of the archive |
| 234 |
- if fis := sums.GetFile(origPath); fis != nil {
|
|
| 235 |
- hash = "file:" + fis.Sum() |
|
| 234 |
+ if fis := sums.GetFile(ci.origPath); fis != nil {
|
|
| 235 |
+ ci.hashPath = "file:" + fis.Sum() |
|
| 236 | 236 |
} |
| 237 | 237 |
} |
| 238 |
- b.Config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, hash, dest)}
|
|
| 239 |
- hit, err := b.probeCache() |
|
| 240 |
- if err != nil {
|
|
| 241 |
- return err |
|
| 242 |
- } |
|
| 243 |
- // If we do not have a hash, never use the cache |
|
| 244 |
- if hit && hash != "" {
|
|
| 245 |
- return nil |
|
| 246 |
- } |
|
| 247 |
- } |
|
| 248 | 238 |
|
| 249 |
- // Create the container |
|
| 250 |
- container, _, err := b.Daemon.Create(b.Config, "") |
|
| 251 |
- if err != nil {
|
|
| 252 |
- return err |
|
| 253 | 239 |
} |
| 254 |
- b.TmpContainers[container.ID] = struct{}{}
|
|
| 255 |
- |
|
| 256 |
- if err := container.Mount(); err != nil {
|
|
| 257 |
- return err |
|
| 258 |
- } |
|
| 259 |
- defer container.Unmount() |
|
| 260 | 240 |
|
| 261 | 241 |
if !allowDecompression || isRemote {
|
| 262 |
- decompress = false |
|
| 263 |
- } |
|
| 264 |
- if err := b.addContext(container, origPath, destPath, decompress); err != nil {
|
|
| 265 |
- return err |
|
| 266 |
- } |
|
| 267 |
- |
|
| 268 |
- if err := b.commit(container.ID, cmd, fmt.Sprintf("%s %s in %s", cmdName, orig, dest)); err != nil {
|
|
| 269 |
- return err |
|
| 242 |
+ ci.decompress = false |
|
| 270 | 243 |
} |
| 271 | 244 |
return nil |
| 272 | 245 |
} |
| ... | ... |
@@ -131,12 +131,13 @@ or |
| 131 | 131 |
interactively, as with the following command: **docker run -t -i image bash** |
| 132 | 132 |
|
| 133 | 133 |
**ADD** |
| 134 |
- --**ADD <src> <dest>** The ADD instruction copies new files from <src> and adds them |
|
| 135 |
- to the filesystem of the container at path <dest>. <src> must be the path to a |
|
| 136 |
- file or directory relative to the source directory that is being built (the |
|
| 137 |
- context of the build) or a remote file URL. <dest> is the absolute path to |
|
| 138 |
- which the source is copied inside the target container. All new files and |
|
| 139 |
- directories are created with mode 0755, with uid and gid 0. |
|
| 134 |
+ --**ADD <src>... <dest>** The ADD instruction copies new files, directories |
|
| 135 |
+ or remote file URLs to the filesystem of the container at path <dest>. |
|
| 136 |
+ Mutliple <src> resources may be specified but if they are files or directories |
|
| 137 |
+ then they must be relative to the source directory that is being built |
|
| 138 |
+ (the context of the build). <dest> is the absolute path to |
|
| 139 |
+ which the source is copied inside the target container. All new files and |
|
| 140 |
+ directories are created with mode 0755, with uid and gid 0. |
|
| 140 | 141 |
|
| 141 | 142 |
**ENTRYPOINT** |
| 142 | 143 |
--**ENTRYPOINT** has two forms: ENTRYPOINT ["executable", "param1", "param2"] |
| ... | ... |
@@ -284,13 +284,15 @@ change them using `docker run --env <key>=<value>`. |
| 284 | 284 |
|
| 285 | 285 |
## ADD |
| 286 | 286 |
|
| 287 |
- ADD <src> <dest> |
|
| 287 |
+ ADD <src>... <dest> |
|
| 288 | 288 |
|
| 289 |
-The `ADD` instruction will copy new files from `<src>` and add them to the |
|
| 290 |
-container's filesystem at path `<dest>`. |
|
| 289 |
+The `ADD` instruction copies new files,directories or remote file URLs to |
|
| 290 |
+the filesystem of the container from `<src>` and add them to the at |
|
| 291 |
+path `<dest>`. |
|
| 291 | 292 |
|
| 292 |
-`<src>` must be the path to a file or directory relative to the source directory |
|
| 293 |
-being built (also called the *context* of the build) or a remote file URL. |
|
| 293 |
+Multiple <src> resource may be specified but if they are files or |
|
| 294 |
+directories then they must be relative to the source directory that is |
|
| 295 |
+being built (the context of the build). |
|
| 294 | 296 |
|
| 295 | 297 |
`<dest>` is the absolute path to which the source will be copied inside the |
| 296 | 298 |
destination container. |
| ... | ... |
@@ -353,6 +355,9 @@ The copy obeys the following rules: |
| 353 | 353 |
will be considered a directory and the contents of `<src>` will be written |
| 354 | 354 |
at `<dest>/base(<src>)`. |
| 355 | 355 |
|
| 356 |
+- If multiple `<src>` resources are specified then `<dest>` must be a |
|
| 357 |
+ directory, and it must end with a slash `/`. |
|
| 358 |
+ |
|
| 356 | 359 |
- If `<dest>` does not end with a trailing slash, it will be considered a |
| 357 | 360 |
regular file and the contents of `<src>` will be written at `<dest>`. |
| 358 | 361 |
|
| ... | ... |
@@ -361,13 +366,15 @@ The copy obeys the following rules: |
| 361 | 361 |
|
| 362 | 362 |
## COPY |
| 363 | 363 |
|
| 364 |
- COPY <src> <dest> |
|
| 364 |
+ COPY <src>... <dest> |
|
| 365 | 365 |
|
| 366 |
-The `COPY` instruction will copy new files from `<src>` and add them to the |
|
| 367 |
-container's filesystem at path `<dest>`. |
|
| 366 |
+The `COPY` instruction copies new files,directories or remote file URLs to |
|
| 367 |
+the filesystem of the container from `<src>` and add them to the at |
|
| 368 |
+path `<dest>`. |
|
| 368 | 369 |
|
| 369 |
-`<src>` must be the path to a file or directory relative to the source directory |
|
| 370 |
-being built (also called the *context* of the build). |
|
| 370 |
+Multiple <src> resource may be specified but if they are files or |
|
| 371 |
+directories then they must be relative to the source directory that is being |
|
| 372 |
+built (the context of the build). |
|
| 371 | 373 |
|
| 372 | 374 |
`<dest>` is the absolute path to which the source will be copied inside the |
| 373 | 375 |
destination container. |
| ... | ... |
@@ -393,6 +400,9 @@ The copy obeys the following rules: |
| 393 | 393 |
will be considered a directory and the contents of `<src>` will be written |
| 394 | 394 |
at `<dest>/base(<src>)`. |
| 395 | 395 |
|
| 396 |
+- If multiple `<src>` resources are specified then `<dest>` must be a |
|
| 397 |
+ directory, and it must end with a slash `/`. |
|
| 398 |
+ |
|
| 396 | 399 |
- If `<dest>` does not end with a trailing slash, it will be considered a |
| 397 | 400 |
regular file and the contents of `<src>` will be written at `<dest>`. |
| 398 | 401 |
|
| 399 | 402 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+FROM busybox |
|
| 1 |
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd |
|
| 2 |
+RUN echo 'dockerio:x:1001:' >> /etc/group |
|
| 3 |
+RUN mkdir /exists |
|
| 4 |
+RUN touch /exists/exists_file |
|
| 5 |
+RUN chown -R dockerio.dockerio /exists |
|
| 6 |
+COPY test_file1 test_file2 /exists/ |
|
| 7 |
+ADD test_file3 test_file4 https://docker.com/robots.txt /exists/ |
|
| 8 |
+RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
|
|
| 9 |
+RUN [ $(ls -l /exists/test_file1 | awk '{print $3":"$4}') = 'root:root' ]
|
|
| 10 |
+RUN [ $(ls -l /exists/test_file2 | awk '{print $3":"$4}') = 'root:root' ]
|
|
| 11 |
+ |
|
| 12 |
+RUN [ $(ls -l /exists/test_file3 | awk '{print $3":"$4}') = 'root:root' ]
|
|
| 13 |
+RUN [ $(ls -l /exists/test_file4 | awk '{print $3":"$4}') = 'root:root' ]
|
|
| 14 |
+RUN [ $(ls -l /exists/robots.txt | awk '{print $3":"$4}') = 'root:root' ]
|
|
| 15 |
+ |
|
| 16 |
+RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
|
| 4 | 21 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,7 @@ |
| 0 |
+FROM busybox |
|
| 1 |
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd |
|
| 2 |
+RUN echo 'dockerio:x:1001:' >> /etc/group |
|
| 3 |
+RUN mkdir /exists |
|
| 4 |
+RUN chown -R dockerio.dockerio /exists |
|
| 5 |
+COPY test_file1 /exists/ |
|
| 6 |
+ADD test_file2 test_file3 /exists/test_file1 |
| ... | ... |
@@ -148,6 +148,66 @@ func TestAddSingleFileToExistDir(t *testing.T) {
|
| 148 | 148 |
logDone("build - add single file to existing dir")
|
| 149 | 149 |
} |
| 150 | 150 |
|
| 151 |
+func TestMultipleFiles(t *testing.T) {
|
|
| 152 |
+ buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy") |
|
| 153 |
+ out, exitCode, err := dockerCmdInDir(t, buildDirectory, "build", "-t", "testaddimg", "MultipleFiles") |
|
| 154 |
+ errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
|
|
| 155 |
+ |
|
| 156 |
+ if err != nil || exitCode != 0 {
|
|
| 157 |
+ t.Fatal("failed to build the image")
|
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ deleteImages("testaddimg")
|
|
| 161 |
+ |
|
| 162 |
+ logDone("build - mulitple file copy/add tests")
|
|
| 163 |
+} |
|
| 164 |
+ |
|
| 165 |
+func TestAddMultipleFilesToFile(t *testing.T) {
|
|
| 166 |
+ name := "testaddmultiplefilestofile" |
|
| 167 |
+ defer deleteImages(name) |
|
| 168 |
+ ctx, err := fakeContext(`FROM scratch |
|
| 169 |
+ ADD file1.txt file2.txt test |
|
| 170 |
+ `, |
|
| 171 |
+ map[string]string{
|
|
| 172 |
+ "file1.txt": "test1", |
|
| 173 |
+ "file2.txt": "test1", |
|
| 174 |
+ }) |
|
| 175 |
+ defer ctx.Close() |
|
| 176 |
+ if err != nil {
|
|
| 177 |
+ t.Fatal(err) |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" |
|
| 181 |
+ if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
|
|
| 182 |
+ t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err)
|
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ logDone("build - multiple add files to file")
|
|
| 186 |
+} |
|
| 187 |
+ |
|
| 188 |
+func TestCopyMultipleFilesToFile(t *testing.T) {
|
|
| 189 |
+ name := "testcopymultiplefilestofile" |
|
| 190 |
+ defer deleteImages(name) |
|
| 191 |
+ ctx, err := fakeContext(`FROM scratch |
|
| 192 |
+ COPY file1.txt file2.txt test |
|
| 193 |
+ `, |
|
| 194 |
+ map[string]string{
|
|
| 195 |
+ "file1.txt": "test1", |
|
| 196 |
+ "file2.txt": "test1", |
|
| 197 |
+ }) |
|
| 198 |
+ defer ctx.Close() |
|
| 199 |
+ if err != nil {
|
|
| 200 |
+ t.Fatal(err) |
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" |
|
| 204 |
+ if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
|
|
| 205 |
+ t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err)
|
|
| 206 |
+ } |
|
| 207 |
+ |
|
| 208 |
+ logDone("build - multiple copy files to file")
|
|
| 209 |
+} |
|
| 210 |
+ |
|
| 151 | 211 |
func TestAddSingleFileToNonExistDir(t *testing.T) {
|
| 152 | 212 |
buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestAdd") |
| 153 | 213 |
out, exitCode, err := dockerCmdInDir(t, buildDirectory, "build", "-t", "testaddimg", "SingleFileToNonExistDir") |
| ... | ... |
@@ -1059,6 +1119,35 @@ func TestBuildADDLocalFileWithCache(t *testing.T) {
|
| 1059 | 1059 |
logDone("build - add local file with cache")
|
| 1060 | 1060 |
} |
| 1061 | 1061 |
|
| 1062 |
+func TestBuildADDMultipleLocalFileWithCache(t *testing.T) {
|
|
| 1063 |
+ name := "testbuildaddmultiplelocalfilewithcache" |
|
| 1064 |
+ defer deleteImages(name) |
|
| 1065 |
+ dockerfile := ` |
|
| 1066 |
+ FROM busybox |
|
| 1067 |
+ MAINTAINER dockerio |
|
| 1068 |
+ ADD foo Dockerfile /usr/lib/bla/ |
|
| 1069 |
+ RUN [ "$(cat /usr/lib/bla/foo)" = "hello" ]` |
|
| 1070 |
+ ctx, err := fakeContext(dockerfile, map[string]string{
|
|
| 1071 |
+ "foo": "hello", |
|
| 1072 |
+ }) |
|
| 1073 |
+ defer ctx.Close() |
|
| 1074 |
+ if err != nil {
|
|
| 1075 |
+ t.Fatal(err) |
|
| 1076 |
+ } |
|
| 1077 |
+ id1, err := buildImageFromContext(name, ctx, true) |
|
| 1078 |
+ if err != nil {
|
|
| 1079 |
+ t.Fatal(err) |
|
| 1080 |
+ } |
|
| 1081 |
+ id2, err := buildImageFromContext(name, ctx, true) |
|
| 1082 |
+ if err != nil {
|
|
| 1083 |
+ t.Fatal(err) |
|
| 1084 |
+ } |
|
| 1085 |
+ if id1 != id2 {
|
|
| 1086 |
+ t.Fatal("The cache should have been used but hasn't.")
|
|
| 1087 |
+ } |
|
| 1088 |
+ logDone("build - add multiple local files with cache")
|
|
| 1089 |
+} |
|
| 1090 |
+ |
|
| 1062 | 1091 |
func TestBuildADDLocalFileWithoutCache(t *testing.T) {
|
| 1063 | 1092 |
name := "testbuildaddlocalfilewithoutcache" |
| 1064 | 1093 |
defer deleteImages(name) |