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