github.com/vincentwoo/docker@v0.7.3-0.20160116130405-82401a4b13c0/api/client/build.go (about) 1 package client 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "bytes" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 17 "github.com/docker/docker/api" 18 "github.com/docker/docker/builder/dockerignore" 19 Cli "github.com/docker/docker/cli" 20 "github.com/docker/docker/opts" 21 "github.com/docker/docker/pkg/archive" 22 "github.com/docker/docker/pkg/fileutils" 23 "github.com/docker/docker/pkg/gitutils" 24 "github.com/docker/docker/pkg/httputils" 25 "github.com/docker/docker/pkg/ioutils" 26 "github.com/docker/docker/pkg/jsonmessage" 27 flag "github.com/docker/docker/pkg/mflag" 28 "github.com/docker/docker/pkg/progress" 29 "github.com/docker/docker/pkg/streamformatter" 30 "github.com/docker/docker/pkg/urlutil" 31 "github.com/docker/docker/reference" 32 runconfigopts "github.com/docker/docker/runconfig/opts" 33 "github.com/docker/engine-api/types" 34 "github.com/docker/engine-api/types/container" 35 "github.com/docker/go-units" 36 ) 37 38 type translatorFunc func(reference.NamedTagged) (reference.Canonical, error) 39 40 // CmdBuild builds a new image from the source code at a given path. 41 // 42 // If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. 43 // 44 // Usage: docker build [OPTIONS] PATH | URL | - 45 func (cli *DockerCli) CmdBuild(args ...string) error { 46 cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true) 47 flTags := opts.NewListOpts(validateTag) 48 cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format") 49 suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the build output and print image ID on success") 50 noCache := cmd.Bool([]string{"-no-cache"}, false, "Do not use cache when building the image") 51 rm := cmd.Bool([]string{"-rm"}, true, "Remove intermediate containers after a successful build") 52 forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") 53 pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") 54 dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") 55 flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") 56 flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") 57 flShmSize := cmd.String([]string{"-shm-size"}, "", "Size of /dev/shm, default value is 64MB") 58 flCPUShares := cmd.Int64([]string{"#c", "-cpu-shares"}, 0, "CPU shares (relative weight)") 59 flCPUPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) period") 60 flCPUQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") 61 flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") 62 flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") 63 flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") 64 flBuildArg := opts.NewListOpts(runconfigopts.ValidateEnv) 65 cmd.Var(&flBuildArg, []string{"-build-arg"}, "Set build-time variables") 66 isolation := cmd.String([]string{"-isolation"}, "", "Container isolation level") 67 68 ulimits := make(map[string]*units.Ulimit) 69 flUlimits := runconfigopts.NewUlimitOpt(&ulimits) 70 cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") 71 72 cmd.Require(flag.Exact, 1) 73 74 // For trusted pull on "FROM <image>" instruction. 75 addTrustedFlags(cmd, true) 76 77 cmd.ParseFlags(args, true) 78 79 var ( 80 context io.ReadCloser 81 isRemote bool 82 err error 83 ) 84 85 _, err = exec.LookPath("git") 86 hasGit := err == nil 87 88 specifiedContext := cmd.Arg(0) 89 90 var ( 91 contextDir string 92 tempDir string 93 relDockerfile string 94 progBuff io.Writer 95 buildBuff io.Writer 96 ) 97 98 progBuff = cli.out 99 buildBuff = cli.out 100 if *suppressOutput { 101 progBuff = bytes.NewBuffer(nil) 102 buildBuff = bytes.NewBuffer(nil) 103 } 104 105 switch { 106 case specifiedContext == "-": 107 context, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName) 108 case urlutil.IsGitURL(specifiedContext) && hasGit: 109 tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName) 110 case urlutil.IsURL(specifiedContext): 111 context, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName) 112 default: 113 contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName) 114 } 115 116 if err != nil { 117 if *suppressOutput && urlutil.IsURL(specifiedContext) { 118 fmt.Fprintln(cli.err, progBuff) 119 } 120 return fmt.Errorf("unable to prepare context: %s", err) 121 } 122 123 if tempDir != "" { 124 defer os.RemoveAll(tempDir) 125 contextDir = tempDir 126 } 127 128 if context == nil { 129 // And canonicalize dockerfile name to a platform-independent one 130 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) 131 if err != nil { 132 return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) 133 } 134 135 f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 136 if err != nil && !os.IsNotExist(err) { 137 return err 138 } 139 140 var excludes []string 141 if err == nil { 142 excludes, err = dockerignore.ReadAll(f) 143 if err != nil { 144 return err 145 } 146 } 147 148 if err := validateContextDirectory(contextDir, excludes); err != nil { 149 return fmt.Errorf("Error checking context: '%s'.", err) 150 } 151 152 // If .dockerignore mentions .dockerignore or the Dockerfile 153 // then make sure we send both files over to the daemon 154 // because Dockerfile is, obviously, needed no matter what, and 155 // .dockerignore is needed to know if either one needs to be 156 // removed. The daemon will remove them for us, if needed, after it 157 // parses the Dockerfile. Ignore errors here, as they will have been 158 // caught by validateContextDirectory above. 159 var includes = []string{"."} 160 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 161 keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 162 if keepThem1 || keepThem2 { 163 includes = append(includes, ".dockerignore", relDockerfile) 164 } 165 166 context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 167 Compression: archive.Uncompressed, 168 ExcludePatterns: excludes, 169 IncludeFiles: includes, 170 }) 171 if err != nil { 172 return err 173 } 174 } 175 176 var resolvedTags []*resolvedTag 177 if isTrusted() { 178 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 179 // Dockerfile which uses trusted pulls. 180 context = replaceDockerfileTarWrapper(context, relDockerfile, cli.trustedReference, &resolvedTags) 181 } 182 183 // Setup an upload progress bar 184 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) 185 186 var body io.Reader = progress.NewProgressReader(context, progressOutput, 0, "", "Sending build context to Docker daemon") 187 188 var memory int64 189 if *flMemoryString != "" { 190 parsedMemory, err := units.RAMInBytes(*flMemoryString) 191 if err != nil { 192 return err 193 } 194 memory = parsedMemory 195 } 196 197 var memorySwap int64 198 if *flMemorySwap != "" { 199 if *flMemorySwap == "-1" { 200 memorySwap = -1 201 } else { 202 parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) 203 if err != nil { 204 return err 205 } 206 memorySwap = parsedMemorySwap 207 } 208 } 209 210 var shmSize int64 211 if *flShmSize != "" { 212 shmSize, err = units.RAMInBytes(*flShmSize) 213 if err != nil { 214 return err 215 } 216 } 217 218 var remoteContext string 219 if isRemote { 220 remoteContext = cmd.Arg(0) 221 } 222 223 options := types.ImageBuildOptions{ 224 Context: body, 225 Memory: memory, 226 MemorySwap: memorySwap, 227 Tags: flTags.GetAll(), 228 SuppressOutput: *suppressOutput, 229 RemoteContext: remoteContext, 230 NoCache: *noCache, 231 Remove: *rm, 232 ForceRemove: *forceRm, 233 PullParent: *pull, 234 IsolationLevel: container.IsolationLevel(*isolation), 235 CPUSetCPUs: *flCPUSetCpus, 236 CPUSetMems: *flCPUSetMems, 237 CPUShares: *flCPUShares, 238 CPUQuota: *flCPUQuota, 239 CPUPeriod: *flCPUPeriod, 240 CgroupParent: *flCgroupParent, 241 Dockerfile: relDockerfile, 242 ShmSize: shmSize, 243 Ulimits: flUlimits.GetList(), 244 BuildArgs: runconfigopts.ConvertKVStringsToMap(flBuildArg.GetAll()), 245 AuthConfigs: cli.configFile.AuthConfigs, 246 } 247 248 response, err := cli.client.ImageBuild(options) 249 if err != nil { 250 return err 251 } 252 253 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut, nil) 254 if err != nil { 255 if jerr, ok := err.(*jsonmessage.JSONError); ok { 256 // If no error code is set, default to 1 257 if jerr.Code == 0 { 258 jerr.Code = 1 259 } 260 if *suppressOutput { 261 fmt.Fprintf(cli.err, "%s%s", progBuff, buildBuff) 262 } 263 return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 264 } 265 } 266 267 // Windows: show error message about modified file permissions if the 268 // daemon isn't running Windows. 269 if response.OSType != "windows" && runtime.GOOS == "windows" { 270 fmt.Fprintln(cli.err, `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) 271 } 272 273 // Everything worked so if -q was provided the output from the daemon 274 // should be just the image ID and we'll print that to stdout. 275 if *suppressOutput { 276 fmt.Fprintf(cli.out, "%s", buildBuff) 277 } 278 279 if isTrusted() { 280 // Since the build was successful, now we must tag any of the resolved 281 // images from the above Dockerfile rewrite. 282 for _, resolved := range resolvedTags { 283 if err := cli.tagTrusted(resolved.digestRef, resolved.tagRef); err != nil { 284 return err 285 } 286 } 287 } 288 289 return nil 290 } 291 292 // validateContextDirectory checks if all the contents of the directory 293 // can be read and returns an error if some files can't be read 294 // symlinks which point to non-existing files don't trigger an error 295 func validateContextDirectory(srcPath string, excludes []string) error { 296 contextRoot, err := getContextRoot(srcPath) 297 if err != nil { 298 return err 299 } 300 return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { 301 // skip this directory/file if it's not in the path, it won't get added to the context 302 if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { 303 return err 304 } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { 305 return err 306 } else if skip { 307 if f.IsDir() { 308 return filepath.SkipDir 309 } 310 return nil 311 } 312 313 if err != nil { 314 if os.IsPermission(err) { 315 return fmt.Errorf("can't stat '%s'", filePath) 316 } 317 if os.IsNotExist(err) { 318 return nil 319 } 320 return err 321 } 322 323 // skip checking if symlinks point to non-existing files, such symlinks can be useful 324 // also skip named pipes, because they hanging on open 325 if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { 326 return nil 327 } 328 329 if !f.IsDir() { 330 currentFile, err := os.Open(filePath) 331 if err != nil && os.IsPermission(err) { 332 return fmt.Errorf("no permission to read from '%s'", filePath) 333 } 334 currentFile.Close() 335 } 336 return nil 337 }) 338 } 339 340 // validateTag checks if the given image name can be resolved. 341 func validateTag(rawRepo string) (string, error) { 342 _, err := reference.ParseNamed(rawRepo) 343 if err != nil { 344 return "", err 345 } 346 347 return rawRepo, nil 348 } 349 350 // isUNC returns true if the path is UNC (one starting \\). It always returns 351 // false on Linux. 352 func isUNC(path string) bool { 353 return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) 354 } 355 356 // getDockerfileRelPath uses the given context directory for a `docker build` 357 // and returns the absolute path to the context directory, the relative path of 358 // the dockerfile in that context directory, and a non-nil error on success. 359 func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { 360 if absContextDir, err = filepath.Abs(givenContextDir); err != nil { 361 return "", "", fmt.Errorf("unable to get absolute context directory: %v", err) 362 } 363 364 // The context dir might be a symbolic link, so follow it to the actual 365 // target directory. 366 // 367 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 368 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 369 // paths (those starting with \\). This hack means that when using links 370 // on UNC paths, they will not be followed. 371 if !isUNC(absContextDir) { 372 absContextDir, err = filepath.EvalSymlinks(absContextDir) 373 if err != nil { 374 return "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err) 375 } 376 } 377 378 stat, err := os.Lstat(absContextDir) 379 if err != nil { 380 return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err) 381 } 382 383 if !stat.IsDir() { 384 return "", "", fmt.Errorf("context must be a directory: %s", absContextDir) 385 } 386 387 absDockerfile := givenDockerfile 388 if absDockerfile == "" { 389 // No -f/--file was specified so use the default relative to the 390 // context directory. 391 absDockerfile = filepath.Join(absContextDir, api.DefaultDockerfileName) 392 393 // Just to be nice ;-) look for 'dockerfile' too but only 394 // use it if we found it, otherwise ignore this check 395 if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { 396 altPath := filepath.Join(absContextDir, strings.ToLower(api.DefaultDockerfileName)) 397 if _, err = os.Lstat(altPath); err == nil { 398 absDockerfile = altPath 399 } 400 } 401 } 402 403 // If not already an absolute path, the Dockerfile path should be joined to 404 // the base directory. 405 if !filepath.IsAbs(absDockerfile) { 406 absDockerfile = filepath.Join(absContextDir, absDockerfile) 407 } 408 409 // Evaluate symlinks in the path to the Dockerfile too. 410 // 411 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 412 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 413 // paths (those starting with \\). This hack means that when using links 414 // on UNC paths, they will not be followed. 415 if !isUNC(absDockerfile) { 416 absDockerfile, err = filepath.EvalSymlinks(absDockerfile) 417 if err != nil { 418 return "", "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) 419 } 420 } 421 422 if _, err := os.Lstat(absDockerfile); err != nil { 423 if os.IsNotExist(err) { 424 return "", "", fmt.Errorf("Cannot locate Dockerfile: %q", absDockerfile) 425 } 426 return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err) 427 } 428 429 if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { 430 return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err) 431 } 432 433 if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { 434 return "", "", fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) 435 } 436 437 return absContextDir, relDockerfile, nil 438 } 439 440 // writeToFile copies from the given reader and writes it to a file with the 441 // given filename. 442 func writeToFile(r io.Reader, filename string) error { 443 file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600)) 444 if err != nil { 445 return fmt.Errorf("unable to create file: %v", err) 446 } 447 defer file.Close() 448 449 if _, err := io.Copy(file, r); err != nil { 450 return fmt.Errorf("unable to write file: %v", err) 451 } 452 453 return nil 454 } 455 456 // getContextFromReader will read the contents of the given reader as either a 457 // Dockerfile or tar archive. Returns a tar archive used as a context and a 458 // path to the Dockerfile inside the tar. 459 func getContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { 460 buf := bufio.NewReader(r) 461 462 magic, err := buf.Peek(archive.HeaderSize) 463 if err != nil && err != io.EOF { 464 return nil, "", fmt.Errorf("failed to peek context header from STDIN: %v", err) 465 } 466 467 if archive.IsArchive(magic) { 468 return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil 469 } 470 471 // Input should be read as a Dockerfile. 472 tmpDir, err := ioutil.TempDir("", "docker-build-context-") 473 if err != nil { 474 return nil, "", fmt.Errorf("unbale to create temporary context directory: %v", err) 475 } 476 477 f, err := os.Create(filepath.Join(tmpDir, api.DefaultDockerfileName)) 478 if err != nil { 479 return nil, "", err 480 } 481 _, err = io.Copy(f, buf) 482 if err != nil { 483 f.Close() 484 return nil, "", err 485 } 486 487 if err := f.Close(); err != nil { 488 return nil, "", err 489 } 490 if err := r.Close(); err != nil { 491 return nil, "", err 492 } 493 494 tar, err := archive.Tar(tmpDir, archive.Uncompressed) 495 if err != nil { 496 return nil, "", err 497 } 498 499 return ioutils.NewReadCloserWrapper(tar, func() error { 500 err := tar.Close() 501 os.RemoveAll(tmpDir) 502 return err 503 }), api.DefaultDockerfileName, nil 504 505 } 506 507 // getContextFromGitURL uses a Git URL as context for a `docker build`. The 508 // git repo is cloned into a temporary directory used as the context directory. 509 // Returns the absolute path to the temporary context directory, the relative 510 // path of the dockerfile in that context directory, and a non-nil error on 511 // success. 512 func getContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { 513 if absContextDir, err = gitutils.Clone(gitURL); err != nil { 514 return "", "", fmt.Errorf("unable to 'git clone' to temporary context directory: %v", err) 515 } 516 517 return getDockerfileRelPath(absContextDir, dockerfileName) 518 } 519 520 // getContextFromURL uses a remote URL as context for a `docker build`. The 521 // remote resource is downloaded as either a Dockerfile or a tar archive. 522 // Returns the tar archive used for the context and a path of the 523 // dockerfile inside the tar. 524 func getContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { 525 response, err := httputils.Download(remoteURL) 526 if err != nil { 527 return nil, "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err) 528 } 529 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) 530 531 // Pass the response body through a progress reader. 532 progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) 533 534 return getContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) 535 } 536 537 // getContextFromLocalDir uses the given local directory as context for a 538 // `docker build`. Returns the absolute path to the local context directory, 539 // the relative path of the dockerfile in that context directory, and a non-nil 540 // error on success. 541 func getContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) { 542 // When using a local context directory, when the Dockerfile is specified 543 // with the `-f/--file` option then it is considered relative to the 544 // current directory and not the context directory. 545 if dockerfileName != "" { 546 if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { 547 return "", "", fmt.Errorf("unable to get absolute path to Dockerfile: %v", err) 548 } 549 } 550 551 return getDockerfileRelPath(localDir, dockerfileName) 552 } 553 554 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) 555 556 // resolvedTag records the repository, tag, and resolved digest reference 557 // from a Dockerfile rewrite. 558 type resolvedTag struct { 559 digestRef reference.Canonical 560 tagRef reference.NamedTagged 561 } 562 563 // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in 564 // "FROM <image>" instructions to a digest reference. `translator` is a 565 // function that takes a repository name and tag reference and returns a 566 // trusted digest reference. 567 func rewriteDockerfileFrom(dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { 568 scanner := bufio.NewScanner(dockerfile) 569 buf := bytes.NewBuffer(nil) 570 571 // Scan the lines of the Dockerfile, looking for a "FROM" line. 572 for scanner.Scan() { 573 line := scanner.Text() 574 575 matches := dockerfileFromLinePattern.FindStringSubmatch(line) 576 if matches != nil && matches[1] != api.NoBaseImageSpecifier { 577 // Replace the line with a resolved "FROM repo@digest" 578 ref, err := reference.ParseNamed(matches[1]) 579 if err != nil { 580 return nil, nil, err 581 } 582 ref = reference.WithDefaultTag(ref) 583 if ref, ok := ref.(reference.NamedTagged); ok && isTrusted() { 584 trustedRef, err := translator(ref) 585 if err != nil { 586 return nil, nil, err 587 } 588 589 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) 590 resolvedTags = append(resolvedTags, &resolvedTag{ 591 digestRef: trustedRef, 592 tagRef: ref, 593 }) 594 } 595 } 596 597 _, err := fmt.Fprintln(buf, line) 598 if err != nil { 599 return nil, nil, err 600 } 601 } 602 603 return buf.Bytes(), resolvedTags, scanner.Err() 604 } 605 606 // replaceDockerfileTarWrapper wraps the given input tar archive stream and 607 // replaces the entry with the given Dockerfile name with the contents of the 608 // new Dockerfile. Returns a new tar archive stream with the replaced 609 // Dockerfile. 610 func replaceDockerfileTarWrapper(inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { 611 pipeReader, pipeWriter := io.Pipe() 612 go func() { 613 tarReader := tar.NewReader(inputTarStream) 614 tarWriter := tar.NewWriter(pipeWriter) 615 616 defer inputTarStream.Close() 617 618 for { 619 hdr, err := tarReader.Next() 620 if err == io.EOF { 621 // Signals end of archive. 622 tarWriter.Close() 623 pipeWriter.Close() 624 return 625 } 626 if err != nil { 627 pipeWriter.CloseWithError(err) 628 return 629 } 630 631 var content io.Reader = tarReader 632 if hdr.Name == dockerfileName { 633 // This entry is the Dockerfile. Since the tar archive was 634 // generated from a directory on the local filesystem, the 635 // Dockerfile will only appear once in the archive. 636 var newDockerfile []byte 637 newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(content, translator) 638 if err != nil { 639 pipeWriter.CloseWithError(err) 640 return 641 } 642 hdr.Size = int64(len(newDockerfile)) 643 content = bytes.NewBuffer(newDockerfile) 644 } 645 646 if err := tarWriter.WriteHeader(hdr); err != nil { 647 pipeWriter.CloseWithError(err) 648 return 649 } 650 651 if _, err := io.Copy(tarWriter, content); err != nil { 652 pipeWriter.CloseWithError(err) 653 return 654 } 655 } 656 }() 657 658 return pipeReader 659 }