github.com/uriddle/docker@v0.0.0-20210926094723-4072e6aeb013/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 specifiedContext := cmd.Arg(0) 86 87 var ( 88 contextDir string 89 tempDir string 90 relDockerfile string 91 progBuff io.Writer 92 buildBuff io.Writer 93 ) 94 95 progBuff = cli.out 96 buildBuff = cli.out 97 if *suppressOutput { 98 progBuff = bytes.NewBuffer(nil) 99 buildBuff = bytes.NewBuffer(nil) 100 } 101 102 switch { 103 case specifiedContext == "-": 104 context, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName) 105 case urlutil.IsGitURL(specifiedContext): 106 tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName) 107 case urlutil.IsURL(specifiedContext): 108 context, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName) 109 default: 110 contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName) 111 } 112 113 if err != nil { 114 if *suppressOutput && urlutil.IsURL(specifiedContext) { 115 fmt.Fprintln(cli.err, progBuff) 116 } 117 return fmt.Errorf("unable to prepare context: %s", err) 118 } 119 120 if tempDir != "" { 121 defer os.RemoveAll(tempDir) 122 contextDir = tempDir 123 } 124 125 if context == nil { 126 // And canonicalize dockerfile name to a platform-independent one 127 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) 128 if err != nil { 129 return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) 130 } 131 132 f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) 133 if err != nil && !os.IsNotExist(err) { 134 return err 135 } 136 137 var excludes []string 138 if err == nil { 139 excludes, err = dockerignore.ReadAll(f) 140 if err != nil { 141 return err 142 } 143 } 144 145 if err := validateContextDirectory(contextDir, excludes); err != nil { 146 return fmt.Errorf("Error checking context: '%s'.", err) 147 } 148 149 // If .dockerignore mentions .dockerignore or the Dockerfile 150 // then make sure we send both files over to the daemon 151 // because Dockerfile is, obviously, needed no matter what, and 152 // .dockerignore is needed to know if either one needs to be 153 // removed. The daemon will remove them for us, if needed, after it 154 // parses the Dockerfile. Ignore errors here, as they will have been 155 // caught by validateContextDirectory above. 156 var includes = []string{"."} 157 keepThem1, _ := fileutils.Matches(".dockerignore", excludes) 158 keepThem2, _ := fileutils.Matches(relDockerfile, excludes) 159 if keepThem1 || keepThem2 { 160 includes = append(includes, ".dockerignore", relDockerfile) 161 } 162 163 context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 164 Compression: archive.Uncompressed, 165 ExcludePatterns: excludes, 166 IncludeFiles: includes, 167 }) 168 if err != nil { 169 return err 170 } 171 } 172 173 var resolvedTags []*resolvedTag 174 if isTrusted() { 175 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 176 // Dockerfile which uses trusted pulls. 177 context = replaceDockerfileTarWrapper(context, relDockerfile, cli.trustedReference, &resolvedTags) 178 } 179 180 // Setup an upload progress bar 181 progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) 182 183 var body io.Reader = progress.NewProgressReader(context, progressOutput, 0, "", "Sending build context to Docker daemon") 184 185 var memory int64 186 if *flMemoryString != "" { 187 parsedMemory, err := units.RAMInBytes(*flMemoryString) 188 if err != nil { 189 return err 190 } 191 memory = parsedMemory 192 } 193 194 var memorySwap int64 195 if *flMemorySwap != "" { 196 if *flMemorySwap == "-1" { 197 memorySwap = -1 198 } else { 199 parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) 200 if err != nil { 201 return err 202 } 203 memorySwap = parsedMemorySwap 204 } 205 } 206 207 var shmSize int64 208 if *flShmSize != "" { 209 shmSize, err = units.RAMInBytes(*flShmSize) 210 if err != nil { 211 return err 212 } 213 } 214 215 var remoteContext string 216 if isRemote { 217 remoteContext = cmd.Arg(0) 218 } 219 220 options := types.ImageBuildOptions{ 221 Context: body, 222 Memory: memory, 223 MemorySwap: memorySwap, 224 Tags: flTags.GetAll(), 225 SuppressOutput: *suppressOutput, 226 RemoteContext: remoteContext, 227 NoCache: *noCache, 228 Remove: *rm, 229 ForceRemove: *forceRm, 230 PullParent: *pull, 231 IsolationLevel: container.IsolationLevel(*isolation), 232 CPUSetCPUs: *flCPUSetCpus, 233 CPUSetMems: *flCPUSetMems, 234 CPUShares: *flCPUShares, 235 CPUQuota: *flCPUQuota, 236 CPUPeriod: *flCPUPeriod, 237 CgroupParent: *flCgroupParent, 238 Dockerfile: relDockerfile, 239 ShmSize: shmSize, 240 Ulimits: flUlimits.GetList(), 241 BuildArgs: runconfigopts.ConvertKVStringsToMap(flBuildArg.GetAll()), 242 AuthConfigs: cli.configFile.AuthConfigs, 243 } 244 245 response, err := cli.client.ImageBuild(options) 246 if err != nil { 247 return err 248 } 249 250 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut, nil) 251 if err != nil { 252 if jerr, ok := err.(*jsonmessage.JSONError); ok { 253 // If no error code is set, default to 1 254 if jerr.Code == 0 { 255 jerr.Code = 1 256 } 257 if *suppressOutput { 258 fmt.Fprintf(cli.err, "%s%s", progBuff, buildBuff) 259 } 260 return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 261 } 262 } 263 264 // Windows: show error message about modified file permissions if the 265 // daemon isn't running Windows. 266 if response.OSType != "windows" && runtime.GOOS == "windows" { 267 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.`) 268 } 269 270 // Everything worked so if -q was provided the output from the daemon 271 // should be just the image ID and we'll print that to stdout. 272 if *suppressOutput { 273 fmt.Fprintf(cli.out, "%s", buildBuff) 274 } 275 276 if isTrusted() { 277 // Since the build was successful, now we must tag any of the resolved 278 // images from the above Dockerfile rewrite. 279 for _, resolved := range resolvedTags { 280 if err := cli.tagTrusted(resolved.digestRef, resolved.tagRef); err != nil { 281 return err 282 } 283 } 284 } 285 286 return nil 287 } 288 289 // validateContextDirectory checks if all the contents of the directory 290 // can be read and returns an error if some files can't be read 291 // symlinks which point to non-existing files don't trigger an error 292 func validateContextDirectory(srcPath string, excludes []string) error { 293 contextRoot, err := getContextRoot(srcPath) 294 if err != nil { 295 return err 296 } 297 return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { 298 // skip this directory/file if it's not in the path, it won't get added to the context 299 if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { 300 return err 301 } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { 302 return err 303 } else if skip { 304 if f.IsDir() { 305 return filepath.SkipDir 306 } 307 return nil 308 } 309 310 if err != nil { 311 if os.IsPermission(err) { 312 return fmt.Errorf("can't stat '%s'", filePath) 313 } 314 if os.IsNotExist(err) { 315 return nil 316 } 317 return err 318 } 319 320 // skip checking if symlinks point to non-existing files, such symlinks can be useful 321 // also skip named pipes, because they hanging on open 322 if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { 323 return nil 324 } 325 326 if !f.IsDir() { 327 currentFile, err := os.Open(filePath) 328 if err != nil && os.IsPermission(err) { 329 return fmt.Errorf("no permission to read from '%s'", filePath) 330 } 331 currentFile.Close() 332 } 333 return nil 334 }) 335 } 336 337 // validateTag checks if the given image name can be resolved. 338 func validateTag(rawRepo string) (string, error) { 339 _, err := reference.ParseNamed(rawRepo) 340 if err != nil { 341 return "", err 342 } 343 344 return rawRepo, nil 345 } 346 347 // isUNC returns true if the path is UNC (one starting \\). It always returns 348 // false on Linux. 349 func isUNC(path string) bool { 350 return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) 351 } 352 353 // getDockerfileRelPath uses the given context directory for a `docker build` 354 // and returns the absolute path to the context directory, the relative path of 355 // the dockerfile in that context directory, and a non-nil error on success. 356 func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { 357 if absContextDir, err = filepath.Abs(givenContextDir); err != nil { 358 return "", "", fmt.Errorf("unable to get absolute context directory: %v", err) 359 } 360 361 // The context dir might be a symbolic link, so follow it to the actual 362 // target directory. 363 // 364 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 365 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 366 // paths (those starting with \\). This hack means that when using links 367 // on UNC paths, they will not be followed. 368 if !isUNC(absContextDir) { 369 absContextDir, err = filepath.EvalSymlinks(absContextDir) 370 if err != nil { 371 return "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err) 372 } 373 } 374 375 stat, err := os.Lstat(absContextDir) 376 if err != nil { 377 return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err) 378 } 379 380 if !stat.IsDir() { 381 return "", "", fmt.Errorf("context must be a directory: %s", absContextDir) 382 } 383 384 absDockerfile := givenDockerfile 385 if absDockerfile == "" { 386 // No -f/--file was specified so use the default relative to the 387 // context directory. 388 absDockerfile = filepath.Join(absContextDir, api.DefaultDockerfileName) 389 390 // Just to be nice ;-) look for 'dockerfile' too but only 391 // use it if we found it, otherwise ignore this check 392 if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { 393 altPath := filepath.Join(absContextDir, strings.ToLower(api.DefaultDockerfileName)) 394 if _, err = os.Lstat(altPath); err == nil { 395 absDockerfile = altPath 396 } 397 } 398 } 399 400 // If not already an absolute path, the Dockerfile path should be joined to 401 // the base directory. 402 if !filepath.IsAbs(absDockerfile) { 403 absDockerfile = filepath.Join(absContextDir, absDockerfile) 404 } 405 406 // Evaluate symlinks in the path to the Dockerfile too. 407 // 408 // FIXME. We use isUNC (always false on non-Windows platforms) to workaround 409 // an issue in golang. On Windows, EvalSymLinks does not work on UNC file 410 // paths (those starting with \\). This hack means that when using links 411 // on UNC paths, they will not be followed. 412 if !isUNC(absDockerfile) { 413 absDockerfile, err = filepath.EvalSymlinks(absDockerfile) 414 if err != nil { 415 return "", "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) 416 } 417 } 418 419 if _, err := os.Lstat(absDockerfile); err != nil { 420 if os.IsNotExist(err) { 421 return "", "", fmt.Errorf("Cannot locate Dockerfile: %q", absDockerfile) 422 } 423 return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err) 424 } 425 426 if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { 427 return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err) 428 } 429 430 if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { 431 return "", "", fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) 432 } 433 434 return absContextDir, relDockerfile, nil 435 } 436 437 // writeToFile copies from the given reader and writes it to a file with the 438 // given filename. 439 func writeToFile(r io.Reader, filename string) error { 440 file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600)) 441 if err != nil { 442 return fmt.Errorf("unable to create file: %v", err) 443 } 444 defer file.Close() 445 446 if _, err := io.Copy(file, r); err != nil { 447 return fmt.Errorf("unable to write file: %v", err) 448 } 449 450 return nil 451 } 452 453 // getContextFromReader will read the contents of the given reader as either a 454 // Dockerfile or tar archive. Returns a tar archive used as a context and a 455 // path to the Dockerfile inside the tar. 456 func getContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { 457 buf := bufio.NewReader(r) 458 459 magic, err := buf.Peek(archive.HeaderSize) 460 if err != nil && err != io.EOF { 461 return nil, "", fmt.Errorf("failed to peek context header from STDIN: %v", err) 462 } 463 464 if archive.IsArchive(magic) { 465 return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil 466 } 467 468 // Input should be read as a Dockerfile. 469 tmpDir, err := ioutil.TempDir("", "docker-build-context-") 470 if err != nil { 471 return nil, "", fmt.Errorf("unbale to create temporary context directory: %v", err) 472 } 473 474 f, err := os.Create(filepath.Join(tmpDir, api.DefaultDockerfileName)) 475 if err != nil { 476 return nil, "", err 477 } 478 _, err = io.Copy(f, buf) 479 if err != nil { 480 f.Close() 481 return nil, "", err 482 } 483 484 if err := f.Close(); err != nil { 485 return nil, "", err 486 } 487 if err := r.Close(); err != nil { 488 return nil, "", err 489 } 490 491 tar, err := archive.Tar(tmpDir, archive.Uncompressed) 492 if err != nil { 493 return nil, "", err 494 } 495 496 return ioutils.NewReadCloserWrapper(tar, func() error { 497 err := tar.Close() 498 os.RemoveAll(tmpDir) 499 return err 500 }), api.DefaultDockerfileName, nil 501 502 } 503 504 // getContextFromGitURL uses a Git URL as context for a `docker build`. The 505 // git repo is cloned into a temporary directory used as the context directory. 506 // Returns the absolute path to the temporary context directory, the relative 507 // path of the dockerfile in that context directory, and a non-nil error on 508 // success. 509 func getContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { 510 if _, err := exec.LookPath("git"); err != nil { 511 return "", "", fmt.Errorf("unable to find 'git': %v", err) 512 } 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 }