github.com/akerouanton/docker@v1.11.0-rc3/builder/dockerfile/internals.go (about) 1 package dockerfile 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/filepath" 16 "runtime" 17 "sort" 18 "strings" 19 "time" 20 21 "github.com/Sirupsen/logrus" 22 "github.com/docker/docker/builder" 23 "github.com/docker/docker/builder/dockerfile/parser" 24 "github.com/docker/docker/pkg/archive" 25 "github.com/docker/docker/pkg/httputils" 26 "github.com/docker/docker/pkg/ioutils" 27 "github.com/docker/docker/pkg/jsonmessage" 28 "github.com/docker/docker/pkg/progress" 29 "github.com/docker/docker/pkg/streamformatter" 30 "github.com/docker/docker/pkg/stringid" 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/runconfig/opts" 35 "github.com/docker/engine-api/types" 36 "github.com/docker/engine-api/types/container" 37 "github.com/docker/engine-api/types/strslice" 38 ) 39 40 func (b *Builder) addLabels() { 41 // merge labels 42 if len(b.options.Labels) > 0 { 43 logrus.Debugf("[BUILDER] setting labels %v", b.options.Labels) 44 if b.runConfig.Labels == nil { 45 b.runConfig.Labels = make(map[string]string) 46 } 47 for kL, vL := range b.options.Labels { 48 b.runConfig.Labels[kL] = vL 49 } 50 } 51 } 52 53 func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) error { 54 if b.disableCommit { 55 return nil 56 } 57 if b.image == "" && !b.noBaseImage { 58 return fmt.Errorf("Please provide a source image with `from` prior to commit") 59 } 60 b.runConfig.Image = b.image 61 62 if id == "" { 63 cmd := b.runConfig.Cmd 64 if runtime.GOOS != "windows" { 65 b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", "#(nop) " + comment} 66 } else { 67 b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S /C", "REM (nop) " + comment} 68 } 69 defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd) 70 71 hit, err := b.probeCache() 72 if err != nil { 73 return err 74 } else if hit { 75 return nil 76 } 77 id, err = b.create() 78 if err != nil { 79 return err 80 } 81 } 82 83 // Note: Actually copy the struct 84 autoConfig := *b.runConfig 85 autoConfig.Cmd = autoCmd 86 87 commitCfg := &types.ContainerCommitConfig{ 88 Author: b.maintainer, 89 Pause: true, 90 Config: &autoConfig, 91 } 92 93 // Commit the container 94 imageID, err := b.docker.Commit(id, commitCfg) 95 if err != nil { 96 return err 97 } 98 99 b.image = imageID 100 return nil 101 } 102 103 type copyInfo struct { 104 builder.FileInfo 105 decompress bool 106 } 107 108 func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalDecompression bool, cmdName string) error { 109 if b.context == nil { 110 return fmt.Errorf("No context given. Impossible to use %s", cmdName) 111 } 112 113 if len(args) < 2 { 114 return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName) 115 } 116 117 // Work in daemon-specific filepath semantics 118 dest := filepath.FromSlash(args[len(args)-1]) // last one is always the dest 119 120 b.runConfig.Image = b.image 121 122 var infos []copyInfo 123 124 // Loop through each src file and calculate the info we need to 125 // do the copy (e.g. hash value if cached). Don't actually do 126 // the copy until we've looked at all src files 127 var err error 128 for _, orig := range args[0 : len(args)-1] { 129 var fi builder.FileInfo 130 decompress := allowLocalDecompression 131 if urlutil.IsURL(orig) { 132 if !allowRemote { 133 return fmt.Errorf("Source can't be a URL for %s", cmdName) 134 } 135 fi, err = b.download(orig) 136 if err != nil { 137 return err 138 } 139 defer os.RemoveAll(filepath.Dir(fi.Path())) 140 decompress = false 141 infos = append(infos, copyInfo{fi, decompress}) 142 continue 143 } 144 // not a URL 145 subInfos, err := b.calcCopyInfo(cmdName, orig, allowLocalDecompression, true) 146 if err != nil { 147 return err 148 } 149 150 infos = append(infos, subInfos...) 151 } 152 153 if len(infos) == 0 { 154 return fmt.Errorf("No source files were specified") 155 } 156 if len(infos) > 1 && !strings.HasSuffix(dest, string(os.PathSeparator)) { 157 return fmt.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName) 158 } 159 160 // For backwards compat, if there's just one info then use it as the 161 // cache look-up string, otherwise hash 'em all into one 162 var srcHash string 163 var origPaths string 164 165 if len(infos) == 1 { 166 fi := infos[0].FileInfo 167 origPaths = fi.Name() 168 if hfi, ok := fi.(builder.Hashed); ok { 169 srcHash = hfi.Hash() 170 } 171 } else { 172 var hashs []string 173 var origs []string 174 for _, info := range infos { 175 fi := info.FileInfo 176 origs = append(origs, fi.Name()) 177 if hfi, ok := fi.(builder.Hashed); ok { 178 hashs = append(hashs, hfi.Hash()) 179 } 180 } 181 hasher := sha256.New() 182 hasher.Write([]byte(strings.Join(hashs, ","))) 183 srcHash = "multi:" + hex.EncodeToString(hasher.Sum(nil)) 184 origPaths = strings.Join(origs, " ") 185 } 186 187 cmd := b.runConfig.Cmd 188 if runtime.GOOS != "windows" { 189 b.runConfig.Cmd = strslice.StrSlice{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest)} 190 } else { 191 b.runConfig.Cmd = strslice.StrSlice{"cmd", "/S", "/C", fmt.Sprintf("REM (nop) %s %s in %s", cmdName, srcHash, dest)} 192 } 193 defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd) 194 195 if hit, err := b.probeCache(); err != nil { 196 return err 197 } else if hit { 198 return nil 199 } 200 201 container, err := b.docker.ContainerCreate(types.ContainerCreateConfig{Config: b.runConfig}) 202 if err != nil { 203 return err 204 } 205 b.tmpContainers[container.ID] = struct{}{} 206 207 comment := fmt.Sprintf("%s %s in %s", cmdName, origPaths, dest) 208 209 // Twiddle the destination when its a relative path - meaning, make it 210 // relative to the WORKINGDIR 211 if !system.IsAbs(dest) { 212 hasSlash := strings.HasSuffix(dest, string(os.PathSeparator)) 213 dest = filepath.Join(string(os.PathSeparator), filepath.FromSlash(b.runConfig.WorkingDir), dest) 214 215 // Make sure we preserve any trailing slash 216 if hasSlash { 217 dest += string(os.PathSeparator) 218 } 219 } 220 221 for _, info := range infos { 222 if err := b.docker.CopyOnBuild(container.ID, dest, info.FileInfo, info.decompress); err != nil { 223 return err 224 } 225 } 226 227 return b.commit(container.ID, cmd, comment) 228 } 229 230 func (b *Builder) download(srcURL string) (fi builder.FileInfo, err error) { 231 // get filename from URL 232 u, err := url.Parse(srcURL) 233 if err != nil { 234 return 235 } 236 path := filepath.FromSlash(u.Path) // Ensure in platform semantics 237 if strings.HasSuffix(path, string(os.PathSeparator)) { 238 path = path[:len(path)-1] 239 } 240 parts := strings.Split(path, string(os.PathSeparator)) 241 filename := parts[len(parts)-1] 242 if filename == "" { 243 err = fmt.Errorf("cannot determine filename from url: %s", u) 244 return 245 } 246 247 // Initiate the download 248 resp, err := httputils.Download(srcURL) 249 if err != nil { 250 return 251 } 252 253 // Prepare file in a tmp dir 254 tmpDir, err := ioutils.TempDir("", "docker-remote") 255 if err != nil { 256 return 257 } 258 defer func() { 259 if err != nil { 260 os.RemoveAll(tmpDir) 261 } 262 }() 263 tmpFileName := filepath.Join(tmpDir, filename) 264 tmpFile, err := os.OpenFile(tmpFileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 265 if err != nil { 266 return 267 } 268 269 stdoutFormatter := b.Stdout.(*streamformatter.StdoutFormatter) 270 progressOutput := stdoutFormatter.StreamFormatter.NewProgressOutput(stdoutFormatter.Writer, true) 271 progressReader := progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Downloading") 272 // Download and dump result to tmp file 273 if _, err = io.Copy(tmpFile, progressReader); err != nil { 274 tmpFile.Close() 275 return 276 } 277 fmt.Fprintln(b.Stdout) 278 // ignoring error because the file was already opened successfully 279 tmpFileSt, err := tmpFile.Stat() 280 if err != nil { 281 return 282 } 283 tmpFile.Close() 284 285 // Set the mtime to the Last-Modified header value if present 286 // Otherwise just remove atime and mtime 287 mTime := time.Time{} 288 289 lastMod := resp.Header.Get("Last-Modified") 290 if lastMod != "" { 291 // If we can't parse it then just let it default to 'zero' 292 // otherwise use the parsed time value 293 if parsedMTime, err := http.ParseTime(lastMod); err == nil { 294 mTime = parsedMTime 295 } 296 } 297 298 if err = system.Chtimes(tmpFileName, mTime, mTime); err != nil { 299 return 300 } 301 302 // Calc the checksum, even if we're using the cache 303 r, err := archive.Tar(tmpFileName, archive.Uncompressed) 304 if err != nil { 305 return 306 } 307 tarSum, err := tarsum.NewTarSum(r, true, tarsum.Version1) 308 if err != nil { 309 return 310 } 311 if _, err = io.Copy(ioutil.Discard, tarSum); err != nil { 312 return 313 } 314 hash := tarSum.Sum(nil) 315 r.Close() 316 return &builder.HashedFileInfo{FileInfo: builder.PathFileInfo{FileInfo: tmpFileSt, FilePath: tmpFileName}, FileHash: hash}, nil 317 } 318 319 func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression, allowWildcards bool) ([]copyInfo, error) { 320 321 // Work in daemon-specific OS filepath semantics 322 origPath = filepath.FromSlash(origPath) 323 324 if origPath != "" && origPath[0] == os.PathSeparator && len(origPath) > 1 { 325 origPath = origPath[1:] 326 } 327 origPath = strings.TrimPrefix(origPath, "."+string(os.PathSeparator)) 328 329 // Deal with wildcards 330 if allowWildcards && containsWildcards(origPath) { 331 var copyInfos []copyInfo 332 if err := b.context.Walk("", func(path string, info builder.FileInfo, err error) error { 333 if err != nil { 334 return err 335 } 336 if info.Name() == "" { 337 // Why are we doing this check? 338 return nil 339 } 340 if match, _ := filepath.Match(origPath, path); !match { 341 return nil 342 } 343 344 // Note we set allowWildcards to false in case the name has 345 // a * in it 346 subInfos, err := b.calcCopyInfo(cmdName, path, allowLocalDecompression, false) 347 if err != nil { 348 return err 349 } 350 copyInfos = append(copyInfos, subInfos...) 351 return nil 352 }); err != nil { 353 return nil, err 354 } 355 return copyInfos, nil 356 } 357 358 // Must be a dir or a file 359 360 statPath, fi, err := b.context.Stat(origPath) 361 if err != nil { 362 return nil, err 363 } 364 365 copyInfos := []copyInfo{{FileInfo: fi, decompress: allowLocalDecompression}} 366 367 hfi, handleHash := fi.(builder.Hashed) 368 if !handleHash { 369 return copyInfos, nil 370 } 371 372 // Deal with the single file case 373 if !fi.IsDir() { 374 hfi.SetHash("file:" + hfi.Hash()) 375 return copyInfos, nil 376 } 377 // Must be a dir 378 var subfiles []string 379 err = b.context.Walk(statPath, func(path string, info builder.FileInfo, err error) error { 380 if err != nil { 381 return err 382 } 383 // we already checked handleHash above 384 subfiles = append(subfiles, info.(builder.Hashed).Hash()) 385 return nil 386 }) 387 if err != nil { 388 return nil, err 389 } 390 391 sort.Strings(subfiles) 392 hasher := sha256.New() 393 hasher.Write([]byte(strings.Join(subfiles, ","))) 394 hfi.SetHash("dir:" + hex.EncodeToString(hasher.Sum(nil))) 395 396 return copyInfos, nil 397 } 398 399 func containsWildcards(name string) bool { 400 for i := 0; i < len(name); i++ { 401 ch := name[i] 402 if ch == '\\' { 403 i++ 404 } else if ch == '*' || ch == '?' || ch == '[' { 405 return true 406 } 407 } 408 return false 409 } 410 411 func (b *Builder) processImageFrom(img builder.Image) error { 412 if img != nil { 413 b.image = img.ImageID() 414 415 if img.RunConfig() != nil { 416 imgConfig := *img.RunConfig() 417 // inherit runConfig labels from the current 418 // state if they've been set already. 419 // Ensures that images with only a FROM 420 // get the labels populated properly. 421 if b.runConfig.Labels != nil { 422 if imgConfig.Labels == nil { 423 imgConfig.Labels = make(map[string]string) 424 } 425 for k, v := range b.runConfig.Labels { 426 imgConfig.Labels[k] = v 427 } 428 } 429 b.runConfig = &imgConfig 430 } 431 } 432 433 // Check to see if we have a default PATH, note that windows won't 434 // have one as its set by HCS 435 if system.DefaultPathEnv != "" { 436 // Convert the slice of strings that represent the current list 437 // of env vars into a map so we can see if PATH is already set. 438 // If its not set then go ahead and give it our default value 439 configEnv := opts.ConvertKVStringsToMap(b.runConfig.Env) 440 if _, ok := configEnv["PATH"]; !ok { 441 b.runConfig.Env = append(b.runConfig.Env, 442 "PATH="+system.DefaultPathEnv) 443 } 444 } 445 446 if img == nil { 447 // Typically this means they used "FROM scratch" 448 return nil 449 } 450 451 // Process ONBUILD triggers if they exist 452 if nTriggers := len(b.runConfig.OnBuild); nTriggers != 0 { 453 word := "trigger" 454 if nTriggers > 1 { 455 word = "triggers" 456 } 457 fmt.Fprintf(b.Stderr, "# Executing %d build %s...\n", nTriggers, word) 458 } 459 460 // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. 461 onBuildTriggers := b.runConfig.OnBuild 462 b.runConfig.OnBuild = []string{} 463 464 // parse the ONBUILD triggers by invoking the parser 465 for _, step := range onBuildTriggers { 466 ast, err := parser.Parse(strings.NewReader(step)) 467 if err != nil { 468 return err 469 } 470 471 for i, n := range ast.Children { 472 switch strings.ToUpper(n.Value) { 473 case "ONBUILD": 474 return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") 475 case "MAINTAINER", "FROM": 476 return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value) 477 } 478 479 if err := b.dispatch(i, n); err != nil { 480 return err 481 } 482 } 483 } 484 485 return nil 486 } 487 488 // probeCache checks if `b.docker` implements builder.ImageCache and image-caching 489 // is enabled (`b.UseCache`). 490 // If so attempts to look up the current `b.image` and `b.runConfig` pair with `b.docker`. 491 // If an image is found, probeCache returns `(true, nil)`. 492 // If no image is found, it returns `(false, nil)`. 493 // If there is any error, it returns `(false, err)`. 494 func (b *Builder) probeCache() (bool, error) { 495 c, ok := b.docker.(builder.ImageCache) 496 if !ok || b.options.NoCache || b.cacheBusted { 497 return false, nil 498 } 499 cache, err := c.GetCachedImageOnBuild(b.image, b.runConfig) 500 if err != nil { 501 return false, err 502 } 503 if len(cache) == 0 { 504 logrus.Debugf("[BUILDER] Cache miss: %s", b.runConfig.Cmd) 505 b.cacheBusted = true 506 return false, nil 507 } 508 509 fmt.Fprintf(b.Stdout, " ---> Using cache\n") 510 logrus.Debugf("[BUILDER] Use cached version: %s", b.runConfig.Cmd) 511 b.image = string(cache) 512 513 return true, nil 514 } 515 516 func (b *Builder) create() (string, error) { 517 if b.image == "" && !b.noBaseImage { 518 return "", fmt.Errorf("Please provide a source image with `from` prior to run") 519 } 520 b.runConfig.Image = b.image 521 522 resources := container.Resources{ 523 CgroupParent: b.options.CgroupParent, 524 CPUShares: b.options.CPUShares, 525 CPUPeriod: b.options.CPUPeriod, 526 CPUQuota: b.options.CPUQuota, 527 CpusetCpus: b.options.CPUSetCPUs, 528 CpusetMems: b.options.CPUSetMems, 529 Memory: b.options.Memory, 530 MemorySwap: b.options.MemorySwap, 531 Ulimits: b.options.Ulimits, 532 } 533 534 // TODO: why not embed a hostconfig in builder? 535 hostConfig := &container.HostConfig{ 536 Isolation: b.options.Isolation, 537 ShmSize: b.options.ShmSize, 538 Resources: resources, 539 } 540 541 config := *b.runConfig 542 543 // Create the container 544 c, err := b.docker.ContainerCreate(types.ContainerCreateConfig{ 545 Config: b.runConfig, 546 HostConfig: hostConfig, 547 }) 548 if err != nil { 549 return "", err 550 } 551 for _, warning := range c.Warnings { 552 fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning) 553 } 554 555 b.tmpContainers[c.ID] = struct{}{} 556 fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(c.ID)) 557 558 // override the entry point that may have been picked up from the base image 559 if err := b.docker.ContainerUpdateCmdOnBuild(c.ID, config.Cmd); err != nil { 560 return "", err 561 } 562 563 return c.ID, nil 564 } 565 566 func (b *Builder) run(cID string) (err error) { 567 errCh := make(chan error) 568 go func() { 569 errCh <- b.docker.ContainerAttachRaw(cID, nil, b.Stdout, b.Stderr, true) 570 }() 571 572 finished := make(chan struct{}) 573 defer close(finished) 574 go func() { 575 select { 576 case <-b.cancelled: 577 logrus.Debugln("Build cancelled, killing and removing container:", cID) 578 b.docker.ContainerKill(cID, 0) 579 b.removeContainer(cID) 580 case <-finished: 581 } 582 }() 583 584 if err := b.docker.ContainerStart(cID, nil); err != nil { 585 return err 586 } 587 588 // Block on reading output from container, stop on err or chan closed 589 if err := <-errCh; err != nil { 590 return err 591 } 592 593 if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 { 594 // TODO: change error type, because jsonmessage.JSONError assumes HTTP 595 return &jsonmessage.JSONError{ 596 Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", strings.Join(b.runConfig.Cmd, " "), ret), 597 Code: ret, 598 } 599 } 600 601 return nil 602 } 603 604 func (b *Builder) removeContainer(c string) error { 605 rmConfig := &types.ContainerRmConfig{ 606 ForceRemove: true, 607 RemoveVolume: true, 608 } 609 if err := b.docker.ContainerRm(c, rmConfig); err != nil { 610 fmt.Fprintf(b.Stdout, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err) 611 return err 612 } 613 return nil 614 } 615 616 func (b *Builder) clearTmp() { 617 for c := range b.tmpContainers { 618 if err := b.removeContainer(c); err != nil { 619 return 620 } 621 delete(b.tmpContainers, c) 622 fmt.Fprintf(b.Stdout, "Removing intermediate container %s\n", stringid.TruncateID(c)) 623 } 624 } 625 626 // readDockerfile reads a Dockerfile from the current context. 627 func (b *Builder) readDockerfile() error { 628 // If no -f was specified then look for 'Dockerfile'. If we can't find 629 // that then look for 'dockerfile'. If neither are found then default 630 // back to 'Dockerfile' and use that in the error message. 631 if b.options.Dockerfile == "" { 632 b.options.Dockerfile = builder.DefaultDockerfileName 633 if _, _, err := b.context.Stat(b.options.Dockerfile); os.IsNotExist(err) { 634 lowercase := strings.ToLower(b.options.Dockerfile) 635 if _, _, err := b.context.Stat(lowercase); err == nil { 636 b.options.Dockerfile = lowercase 637 } 638 } 639 } 640 641 f, err := b.context.Open(b.options.Dockerfile) 642 if err != nil { 643 if os.IsNotExist(err) { 644 return fmt.Errorf("Cannot locate specified Dockerfile: %s", b.options.Dockerfile) 645 } 646 return err 647 } 648 if f, ok := f.(*os.File); ok { 649 // ignoring error because Open already succeeded 650 fi, err := f.Stat() 651 if err != nil { 652 return fmt.Errorf("Unexpected error reading Dockerfile: %v", err) 653 } 654 if fi.Size() == 0 { 655 return fmt.Errorf("The Dockerfile (%s) cannot be empty", b.options.Dockerfile) 656 } 657 } 658 b.dockerfile, err = parser.Parse(f) 659 f.Close() 660 if err != nil { 661 return err 662 } 663 664 // After the Dockerfile has been parsed, we need to check the .dockerignore 665 // file for either "Dockerfile" or ".dockerignore", and if either are 666 // present then erase them from the build context. These files should never 667 // have been sent from the client but we did send them to make sure that 668 // we had the Dockerfile to actually parse, and then we also need the 669 // .dockerignore file to know whether either file should be removed. 670 // Note that this assumes the Dockerfile has been read into memory and 671 // is now safe to be removed. 672 if dockerIgnore, ok := b.context.(builder.DockerIgnoreContext); ok { 673 dockerIgnore.Process([]string{b.options.Dockerfile}) 674 } 675 return nil 676 } 677 678 // determine if build arg is part of built-in args or user 679 // defined args in Dockerfile at any point in time. 680 func (b *Builder) isBuildArgAllowed(arg string) bool { 681 if _, ok := BuiltinAllowedBuildArgs[arg]; ok { 682 return true 683 } 684 if _, ok := b.allowedBuildArgs[arg]; ok { 685 return true 686 } 687 return false 688 }