github.com/guilhermebr/docker@v1.4.2-0.20150428121140-67da055cebca/builder/internals.go (about) 1 package builder 2 3 // internals for handling commands. Covers many areas and a lot of 4 // non-contiguous functionality. Please read the comments. 5 6 import ( 7 "crypto/sha256" 8 "encoding/hex" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/url" 14 "os" 15 "path" 16 "path/filepath" 17 "sort" 18 "strings" 19 "syscall" 20 "time" 21 22 "github.com/Sirupsen/logrus" 23 "github.com/docker/docker/builder/parser" 24 "github.com/docker/docker/daemon" 25 "github.com/docker/docker/graph" 26 imagepkg "github.com/docker/docker/image" 27 "github.com/docker/docker/pkg/archive" 28 "github.com/docker/docker/pkg/chrootarchive" 29 "github.com/docker/docker/pkg/httputils" 30 "github.com/docker/docker/pkg/ioutils" 31 "github.com/docker/docker/pkg/jsonmessage" 32 "github.com/docker/docker/pkg/parsers" 33 "github.com/docker/docker/pkg/progressreader" 34 "github.com/docker/docker/pkg/stringid" 35 "github.com/docker/docker/pkg/symlink" 36 "github.com/docker/docker/pkg/system" 37 "github.com/docker/docker/pkg/tarsum" 38 "github.com/docker/docker/pkg/urlutil" 39 "github.com/docker/docker/registry" 40 "github.com/docker/docker/runconfig" 41 ) 42 43 func (b *Builder) readContext(context io.Reader) error { 44 tmpdirPath, err := ioutil.TempDir("", "docker-build") 45 if err != nil { 46 return err 47 } 48 49 decompressedStream, err := archive.DecompressStream(context) 50 if err != nil { 51 return err 52 } 53 54 if b.context, err = tarsum.NewTarSum(decompressedStream, true, tarsum.Version0); err != nil { 55 return err 56 } 57 58 if err := chrootarchive.Untar(b.context, tmpdirPath, nil); err != nil { 59 return err 60 } 61 62 b.contextPath = tmpdirPath 63 return nil 64 } 65 66 func (b *Builder) commit(id string, autoCmd *runconfig.Command, comment string) error { 67 if b.disableCommit { 68 return nil 69 } 70 if b.image == "" && !b.noBaseImage { 71 return fmt.Errorf("Please provide a source image with `from` prior to commit") 72 } 73 b.Config.Image = b.image 74 if id == "" { 75 cmd := b.Config.Cmd 76 b.Config.Cmd = runconfig.NewCommand("/bin/sh", "-c", "#(nop) "+comment) 77 defer func(cmd *runconfig.Command) { b.Config.Cmd = cmd }(cmd) 78 79 hit, err := b.probeCache() 80 if err != nil { 81 return err 82 } 83 if hit { 84 return nil 85 } 86 87 container, err := b.create() 88 if err != nil { 89 return err 90 } 91 id = container.ID 92 93 if err := container.Mount(); err != nil { 94 return err 95 } 96 defer container.Unmount() 97 } 98 container, err := b.Daemon.Get(id) 99 if err != nil { 100 return err 101 } 102 103 // Note: Actually copy the struct 104 autoConfig := *b.Config 105 autoConfig.Cmd = autoCmd 106 107 // Commit the container 108 image, err := b.Daemon.Commit(container, "", "", "", b.maintainer, true, &autoConfig) 109 if err != nil { 110 return err 111 } 112 b.image = image.ID 113 return nil 114 } 115 116 type copyInfo struct { 117 origPath string 118 destPath string 119 hash string 120 decompress bool 121 tmpDir string 122 } 123 124 func (b *Builder) runContextCommand(args []string, allowRemote bool, allowDecompression bool, cmdName string) error { 125 if b.context == nil { 126 return fmt.Errorf("No context given. Impossible to use %s", cmdName) 127 } 128 129 if len(args) < 2 { 130 return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName) 131 } 132 133 dest := args[len(args)-1] // last one is always the dest 134 135 copyInfos := []*copyInfo{} 136 137 b.Config.Image = b.image 138 139 defer func() { 140 for _, ci := range copyInfos { 141 if ci.tmpDir != "" { 142 os.RemoveAll(ci.tmpDir) 143 } 144 } 145 }() 146 147 // Loop through each src file and calculate the info we need to 148 // do the copy (e.g. hash value if cached). Don't actually do 149 // the copy until we've looked at all src files 150 for _, orig := range args[0 : len(args)-1] { 151 if err := calcCopyInfo( 152 b, 153 cmdName, 154 ©Infos, 155 orig, 156 dest, 157 allowRemote, 158 allowDecompression, 159 ); err != nil { 160 return err 161 } 162 } 163 164 if len(copyInfos) == 0 { 165 return fmt.Errorf("No source files were specified") 166 } 167 168 if len(copyInfos) > 1 && !strings.HasSuffix(dest, "/") { 169 return fmt.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName) 170 } 171 172 // For backwards compat, if there's just one CI then use it as the 173 // cache look-up string, otherwise hash 'em all into one 174 var srcHash string 175 var origPaths string 176 177 if len(copyInfos) == 1 { 178 srcHash = copyInfos[0].hash 179 origPaths = copyInfos[0].origPath 180 } else { 181 var hashs []string 182 var origs []string 183 for _, ci := range copyInfos { 184 hashs = append(hashs, ci.hash) 185 origs = append(origs, ci.origPath) 186 } 187 hasher := sha256.New() 188 hasher.Write([]byte(strings.Join(hashs, ","))) 189 srcHash = "multi:" + hex.EncodeToString(hasher.Sum(nil)) 190 origPaths = strings.Join(origs, " ") 191 } 192 193 cmd := b.Config.Cmd 194 b.Config.Cmd = runconfig.NewCommand("/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest)) 195 defer func(cmd *runconfig.Command) { b.Config.Cmd = cmd }(cmd) 196 197 hit, err := b.probeCache() 198 if err != nil { 199 return err 200 } 201 202 if hit { 203 return nil 204 } 205 206 container, _, err := b.Daemon.Create(b.Config, nil, "") 207 if err != nil { 208 return err 209 } 210 b.TmpContainers[container.ID] = struct{}{} 211 212 if err := container.Mount(); err != nil { 213 return err 214 } 215 defer container.Unmount() 216 217 for _, ci := range copyInfos { 218 if err := b.addContext(container, ci.origPath, ci.destPath, ci.decompress); err != nil { 219 return err 220 } 221 } 222 223 if err := b.commit(container.ID, cmd, fmt.Sprintf("%s %s in %s", cmdName, origPaths, dest)); err != nil { 224 return err 225 } 226 return nil 227 } 228 229 func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath string, destPath string, allowRemote bool, allowDecompression bool) error { 230 231 if origPath != "" && origPath[0] == '/' && len(origPath) > 1 { 232 origPath = origPath[1:] 233 } 234 origPath = strings.TrimPrefix(origPath, "./") 235 236 // Twiddle the destPath when its a relative path - meaning, make it 237 // relative to the WORKINGDIR 238 if !filepath.IsAbs(destPath) { 239 hasSlash := strings.HasSuffix(destPath, "/") 240 destPath = filepath.Join("/", b.Config.WorkingDir, destPath) 241 242 // Make sure we preserve any trailing slash 243 if hasSlash { 244 destPath += "/" 245 } 246 } 247 248 // In the remote/URL case, download it and gen its hashcode 249 if urlutil.IsURL(origPath) { 250 if !allowRemote { 251 return fmt.Errorf("Source can't be a URL for %s", cmdName) 252 } 253 254 ci := copyInfo{} 255 ci.origPath = origPath 256 ci.hash = origPath // default to this but can change 257 ci.destPath = destPath 258 ci.decompress = false 259 *cInfos = append(*cInfos, &ci) 260 261 // Initiate the download 262 resp, err := httputils.Download(ci.origPath) 263 if err != nil { 264 return err 265 } 266 267 // Create a tmp dir 268 tmpDirName, err := ioutil.TempDir(b.contextPath, "docker-remote") 269 if err != nil { 270 return err 271 } 272 ci.tmpDir = tmpDirName 273 274 // Create a tmp file within our tmp dir 275 tmpFileName := path.Join(tmpDirName, "tmp") 276 tmpFile, err := os.OpenFile(tmpFileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 277 if err != nil { 278 return err 279 } 280 281 // Download and dump result to tmp file 282 if _, err := io.Copy(tmpFile, progressreader.New(progressreader.Config{ 283 In: resp.Body, 284 Out: b.OutOld, 285 Formatter: b.StreamFormatter, 286 Size: int(resp.ContentLength), 287 NewLines: true, 288 ID: "", 289 Action: "Downloading", 290 })); err != nil { 291 tmpFile.Close() 292 return err 293 } 294 fmt.Fprintf(b.OutStream, "\n") 295 tmpFile.Close() 296 297 // Set the mtime to the Last-Modified header value if present 298 // Otherwise just remove atime and mtime 299 times := make([]syscall.Timespec, 2) 300 301 lastMod := resp.Header.Get("Last-Modified") 302 if lastMod != "" { 303 mTime, err := http.ParseTime(lastMod) 304 // If we can't parse it then just let it default to 'zero' 305 // otherwise use the parsed time value 306 if err == nil { 307 times[1] = syscall.NsecToTimespec(mTime.UnixNano()) 308 } 309 } 310 311 if err := system.UtimesNano(tmpFileName, times); err != nil { 312 return err 313 } 314 315 ci.origPath = path.Join(filepath.Base(tmpDirName), filepath.Base(tmpFileName)) 316 317 // If the destination is a directory, figure out the filename. 318 if strings.HasSuffix(ci.destPath, "/") { 319 u, err := url.Parse(origPath) 320 if err != nil { 321 return err 322 } 323 path := u.Path 324 if strings.HasSuffix(path, "/") { 325 path = path[:len(path)-1] 326 } 327 parts := strings.Split(path, "/") 328 filename := parts[len(parts)-1] 329 if filename == "" { 330 return fmt.Errorf("cannot determine filename from url: %s", u) 331 } 332 ci.destPath = ci.destPath + filename 333 } 334 335 // Calc the checksum, even if we're using the cache 336 r, err := archive.Tar(tmpFileName, archive.Uncompressed) 337 if err != nil { 338 return err 339 } 340 tarSum, err := tarsum.NewTarSum(r, true, tarsum.Version0) 341 if err != nil { 342 return err 343 } 344 if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { 345 return err 346 } 347 ci.hash = tarSum.Sum(nil) 348 r.Close() 349 350 return nil 351 } 352 353 // Deal with wildcards 354 if ContainsWildcards(origPath) { 355 for _, fileInfo := range b.context.GetSums() { 356 if fileInfo.Name() == "" { 357 continue 358 } 359 match, _ := path.Match(origPath, fileInfo.Name()) 360 if !match { 361 continue 362 } 363 364 calcCopyInfo(b, cmdName, cInfos, fileInfo.Name(), destPath, allowRemote, allowDecompression) 365 } 366 return nil 367 } 368 369 // Must be a dir or a file 370 371 if err := b.checkPathForAddition(origPath); err != nil { 372 return err 373 } 374 fi, _ := os.Stat(path.Join(b.contextPath, origPath)) 375 376 ci := copyInfo{} 377 ci.origPath = origPath 378 ci.hash = origPath 379 ci.destPath = destPath 380 ci.decompress = allowDecompression 381 *cInfos = append(*cInfos, &ci) 382 383 // Deal with the single file case 384 if !fi.IsDir() { 385 // This will match first file in sums of the archive 386 fis := b.context.GetSums().GetFile(ci.origPath) 387 if fis != nil { 388 ci.hash = "file:" + fis.Sum() 389 } 390 return nil 391 } 392 393 // Must be a dir 394 var subfiles []string 395 absOrigPath := path.Join(b.contextPath, ci.origPath) 396 397 // Add a trailing / to make sure we only pick up nested files under 398 // the dir and not sibling files of the dir that just happen to 399 // start with the same chars 400 if !strings.HasSuffix(absOrigPath, "/") { 401 absOrigPath += "/" 402 } 403 404 // Need path w/o / too to find matching dir w/o trailing / 405 absOrigPathNoSlash := absOrigPath[:len(absOrigPath)-1] 406 407 for _, fileInfo := range b.context.GetSums() { 408 absFile := path.Join(b.contextPath, fileInfo.Name()) 409 // Any file in the context that starts with the given path will be 410 // picked up and its hashcode used. However, we'll exclude the 411 // root dir itself. We do this for a coupel of reasons: 412 // 1 - ADD/COPY will not copy the dir itself, just its children 413 // so there's no reason to include it in the hash calc 414 // 2 - the metadata on the dir will change when any child file 415 // changes. This will lead to a miss in the cache check if that 416 // child file is in the .dockerignore list. 417 if strings.HasPrefix(absFile, absOrigPath) && absFile != absOrigPathNoSlash { 418 subfiles = append(subfiles, fileInfo.Sum()) 419 } 420 } 421 sort.Strings(subfiles) 422 hasher := sha256.New() 423 hasher.Write([]byte(strings.Join(subfiles, ","))) 424 ci.hash = "dir:" + hex.EncodeToString(hasher.Sum(nil)) 425 426 return nil 427 } 428 429 func ContainsWildcards(name string) bool { 430 for i := 0; i < len(name); i++ { 431 ch := name[i] 432 if ch == '\\' { 433 i++ 434 } else if ch == '*' || ch == '?' || ch == '[' { 435 return true 436 } 437 } 438 return false 439 } 440 441 func (b *Builder) pullImage(name string) (*imagepkg.Image, error) { 442 remote, tag := parsers.ParseRepositoryTag(name) 443 if tag == "" { 444 tag = "latest" 445 } 446 447 pullRegistryAuth := b.AuthConfig 448 if len(b.ConfigFile.AuthConfigs) > 0 { 449 // The request came with a full auth config file, we prefer to use that 450 repoInfo, err := b.Daemon.RegistryService.ResolveRepository(remote) 451 if err != nil { 452 return nil, err 453 } 454 resolvedAuth := registry.ResolveAuthConfig(b.ConfigFile, repoInfo.Index) 455 pullRegistryAuth = &resolvedAuth 456 } 457 458 imagePullConfig := &graph.ImagePullConfig{ 459 Parallel: true, 460 AuthConfig: pullRegistryAuth, 461 OutStream: ioutils.NopWriteCloser(b.OutOld), 462 Json: b.StreamFormatter.Json(), 463 } 464 465 if err := b.Daemon.Repositories().Pull(remote, tag, imagePullConfig); err != nil { 466 return nil, err 467 } 468 469 image, err := b.Daemon.Repositories().LookupImage(name) 470 if err != nil { 471 return nil, err 472 } 473 474 return image, nil 475 } 476 477 func (b *Builder) processImageFrom(img *imagepkg.Image) error { 478 b.image = img.ID 479 480 if img.Config != nil { 481 b.Config = img.Config 482 } 483 484 if len(b.Config.Env) == 0 { 485 b.Config.Env = append(b.Config.Env, "PATH="+daemon.DefaultPathEnv) 486 } 487 488 // Process ONBUILD triggers if they exist 489 if nTriggers := len(b.Config.OnBuild); nTriggers != 0 { 490 fmt.Fprintf(b.ErrStream, "# Executing %d build triggers\n", nTriggers) 491 } 492 493 // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. 494 onBuildTriggers := b.Config.OnBuild 495 b.Config.OnBuild = []string{} 496 497 // parse the ONBUILD triggers by invoking the parser 498 for stepN, step := range onBuildTriggers { 499 ast, err := parser.Parse(strings.NewReader(step)) 500 if err != nil { 501 return err 502 } 503 504 for i, n := range ast.Children { 505 switch strings.ToUpper(n.Value) { 506 case "ONBUILD": 507 return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") 508 case "MAINTAINER", "FROM": 509 return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value) 510 } 511 512 fmt.Fprintf(b.OutStream, "Trigger %d, %s\n", stepN, step) 513 514 if err := b.dispatch(i, n); err != nil { 515 return err 516 } 517 } 518 } 519 520 return nil 521 } 522 523 // probeCache checks to see if image-caching is enabled (`b.UtilizeCache`) 524 // and if so attempts to look up the current `b.image` and `b.Config` pair 525 // in the current server `b.Daemon`. If an image is found, probeCache returns 526 // `(true, nil)`. If no image is found, it returns `(false, nil)`. If there 527 // is any error, it returns `(false, err)`. 528 func (b *Builder) probeCache() (bool, error) { 529 if !b.UtilizeCache || b.cacheBusted { 530 return false, nil 531 } 532 533 cache, err := b.Daemon.ImageGetCached(b.image, b.Config) 534 if err != nil { 535 return false, err 536 } 537 if cache == nil { 538 logrus.Debugf("[BUILDER] Cache miss") 539 b.cacheBusted = true 540 return false, nil 541 } 542 543 fmt.Fprintf(b.OutStream, " ---> Using cache\n") 544 logrus.Debugf("[BUILDER] Use cached version") 545 b.image = cache.ID 546 return true, nil 547 } 548 549 func (b *Builder) create() (*daemon.Container, error) { 550 if b.image == "" && !b.noBaseImage { 551 return nil, fmt.Errorf("Please provide a source image with `from` prior to run") 552 } 553 b.Config.Image = b.image 554 555 hostConfig := &runconfig.HostConfig{ 556 CpuShares: b.cpuShares, 557 CpuQuota: b.cpuQuota, 558 CpusetCpus: b.cpuSetCpus, 559 CpusetMems: b.cpuSetMems, 560 Memory: b.memory, 561 MemorySwap: b.memorySwap, 562 } 563 564 config := *b.Config 565 566 // Create the container 567 c, warnings, err := b.Daemon.Create(b.Config, hostConfig, "") 568 if err != nil { 569 return nil, err 570 } 571 for _, warning := range warnings { 572 fmt.Fprintf(b.OutStream, " ---> [Warning] %s\n", warning) 573 } 574 575 b.TmpContainers[c.ID] = struct{}{} 576 fmt.Fprintf(b.OutStream, " ---> Running in %s\n", stringid.TruncateID(c.ID)) 577 578 if config.Cmd.Len() > 0 { 579 // override the entry point that may have been picked up from the base image 580 s := config.Cmd.Slice() 581 c.Path = s[0] 582 c.Args = s[1:] 583 } else { 584 config.Cmd = runconfig.NewCommand() 585 } 586 587 return c, nil 588 } 589 590 func (b *Builder) run(c *daemon.Container) error { 591 var errCh chan error 592 if b.Verbose { 593 errCh = c.Attach(nil, b.OutStream, b.ErrStream) 594 } 595 596 //start the container 597 if err := c.Start(); err != nil { 598 return err 599 } 600 601 finished := make(chan struct{}) 602 defer close(finished) 603 go func() { 604 select { 605 case <-b.cancelled: 606 logrus.Debugln("Build cancelled, killing container:", c.ID) 607 c.Kill() 608 case <-finished: 609 } 610 }() 611 612 if b.Verbose { 613 // Block on reading output from container, stop on err or chan closed 614 if err := <-errCh; err != nil { 615 return err 616 } 617 } 618 619 // Wait for it to finish 620 if ret, _ := c.WaitStop(-1 * time.Second); ret != 0 { 621 return &jsonmessage.JSONError{ 622 Message: fmt.Sprintf("The command %v returned a non-zero code: %d", b.Config.Cmd, ret), 623 Code: ret, 624 } 625 } 626 627 return nil 628 } 629 630 func (b *Builder) checkPathForAddition(orig string) error { 631 origPath := path.Join(b.contextPath, orig) 632 origPath, err := filepath.EvalSymlinks(origPath) 633 if err != nil { 634 if os.IsNotExist(err) { 635 return fmt.Errorf("%s: no such file or directory", orig) 636 } 637 return err 638 } 639 if !strings.HasPrefix(origPath, b.contextPath) { 640 return fmt.Errorf("Forbidden path outside the build context: %s (%s)", orig, origPath) 641 } 642 if _, err := os.Stat(origPath); err != nil { 643 if os.IsNotExist(err) { 644 return fmt.Errorf("%s: no such file or directory", orig) 645 } 646 return err 647 } 648 return nil 649 } 650 651 func (b *Builder) addContext(container *daemon.Container, orig, dest string, decompress bool) error { 652 var ( 653 err error 654 destExists = true 655 origPath = path.Join(b.contextPath, orig) 656 destPath = path.Join(container.RootfsPath(), dest) 657 ) 658 659 if destPath != container.RootfsPath() { 660 destPath, err = symlink.FollowSymlinkInScope(destPath, container.RootfsPath()) 661 if err != nil { 662 return err 663 } 664 } 665 666 // Preserve the trailing '/' 667 if strings.HasSuffix(dest, "/") || dest == "." { 668 destPath = destPath + "/" 669 } 670 671 destStat, err := os.Stat(destPath) 672 if err != nil { 673 if !os.IsNotExist(err) { 674 return err 675 } 676 destExists = false 677 } 678 679 fi, err := os.Stat(origPath) 680 if err != nil { 681 if os.IsNotExist(err) { 682 return fmt.Errorf("%s: no such file or directory", orig) 683 } 684 return err 685 } 686 687 if fi.IsDir() { 688 return copyAsDirectory(origPath, destPath, destExists) 689 } 690 691 // If we are adding a remote file (or we've been told not to decompress), do not try to untar it 692 if decompress { 693 // First try to unpack the source as an archive 694 // to support the untar feature we need to clean up the path a little bit 695 // because tar is very forgiving. First we need to strip off the archive's 696 // filename from the path but this is only added if it does not end in / . 697 tarDest := destPath 698 if strings.HasSuffix(tarDest, "/") { 699 tarDest = filepath.Dir(destPath) 700 } 701 702 // try to successfully untar the orig 703 if err := chrootarchive.UntarPath(origPath, tarDest); err == nil { 704 return nil 705 } else if err != io.EOF { 706 logrus.Debugf("Couldn't untar %s to %s: %s", origPath, tarDest, err) 707 } 708 } 709 710 if err := os.MkdirAll(path.Dir(destPath), 0755); err != nil { 711 return err 712 } 713 if err := chrootarchive.CopyWithTar(origPath, destPath); err != nil { 714 return err 715 } 716 717 resPath := destPath 718 if destExists && destStat.IsDir() { 719 resPath = path.Join(destPath, path.Base(origPath)) 720 } 721 722 return fixPermissions(origPath, resPath, 0, 0, destExists) 723 } 724 725 func copyAsDirectory(source, destination string, destExisted bool) error { 726 if err := chrootarchive.CopyWithTar(source, destination); err != nil { 727 return err 728 } 729 return fixPermissions(source, destination, 0, 0, destExisted) 730 } 731 732 func fixPermissions(source, destination string, uid, gid int, destExisted bool) error { 733 // If the destination didn't already exist, or the destination isn't a 734 // directory, then we should Lchown the destination. Otherwise, we shouldn't 735 // Lchown the destination. 736 destStat, err := os.Stat(destination) 737 if err != nil { 738 // This should *never* be reached, because the destination must've already 739 // been created while untar-ing the context. 740 return err 741 } 742 doChownDestination := !destExisted || !destStat.IsDir() 743 744 // We Walk on the source rather than on the destination because we don't 745 // want to change permissions on things we haven't created or modified. 746 return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error { 747 // Do not alter the walk root iff. it existed before, as it doesn't fall under 748 // the domain of "things we should chown". 749 if !doChownDestination && (source == fullpath) { 750 return nil 751 } 752 753 // Path is prefixed by source: substitute with destination instead. 754 cleaned, err := filepath.Rel(source, fullpath) 755 if err != nil { 756 return err 757 } 758 759 fullpath = path.Join(destination, cleaned) 760 return os.Lchown(fullpath, uid, gid) 761 }) 762 } 763 764 func (b *Builder) clearTmp() { 765 for c := range b.TmpContainers { 766 tmp, err := b.Daemon.Get(c) 767 if err != nil { 768 fmt.Fprint(b.OutStream, err.Error()) 769 } 770 771 if err := b.Daemon.Rm(tmp); err != nil { 772 fmt.Fprintf(b.OutStream, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err) 773 return 774 } 775 b.Daemon.DeleteVolumes(tmp.VolumePaths()) 776 delete(b.TmpContainers, c) 777 fmt.Fprintf(b.OutStream, "Removing intermediate container %s\n", stringid.TruncateID(c)) 778 } 779 }