github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/command/image/build.go (about) 1 package image 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "bytes" 7 "context" 8 "encoding/csv" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "regexp" 16 "runtime" 17 "strings" 18 19 "github.com/docker/cli/cli" 20 "github.com/docker/cli/cli/command" 21 "github.com/docker/cli/cli/command/image/build" 22 "github.com/docker/cli/opts" 23 "github.com/docker/distribution/reference" 24 "github.com/docker/docker/api" 25 "github.com/docker/docker/api/types" 26 "github.com/docker/docker/api/types/container" 27 "github.com/docker/docker/pkg/archive" 28 "github.com/docker/docker/pkg/idtools" 29 "github.com/docker/docker/pkg/jsonmessage" 30 "github.com/docker/docker/pkg/progress" 31 "github.com/docker/docker/pkg/streamformatter" 32 "github.com/docker/docker/pkg/urlutil" 33 units "github.com/docker/go-units" 34 "github.com/pkg/errors" 35 "github.com/spf13/cobra" 36 ) 37 38 var errStdinConflict = errors.New("invalid argument: can't use stdin for both build context and dockerfile") 39 40 type buildOptions struct { 41 context string 42 dockerfileName string 43 tags opts.ListOpts 44 labels opts.ListOpts 45 buildArgs opts.ListOpts 46 extraHosts opts.ListOpts 47 ulimits *opts.UlimitOpt 48 memory opts.MemBytes 49 memorySwap opts.MemSwapBytes 50 shmSize opts.MemBytes 51 cpuShares int64 52 cpuPeriod int64 53 cpuQuota int64 54 cpuSetCpus string 55 cpuSetMems string 56 cgroupParent string 57 isolation string 58 quiet bool 59 noCache bool 60 progress string 61 rm bool 62 forceRm bool 63 pull bool 64 cacheFrom []string 65 compress bool 66 securityOpt []string 67 networkMode string 68 squash bool 69 target string 70 imageIDFile string 71 stream bool 72 platform string 73 untrusted bool 74 secrets []string 75 ssh []string 76 outputs []string 77 } 78 79 // dockerfileFromStdin returns true when the user specified that the Dockerfile 80 // should be read from stdin instead of a file 81 func (o buildOptions) dockerfileFromStdin() bool { 82 return o.dockerfileName == "-" 83 } 84 85 // contextFromStdin returns true when the user specified that the build context 86 // should be read from stdin 87 func (o buildOptions) contextFromStdin() bool { 88 return o.context == "-" 89 } 90 91 func newBuildOptions() buildOptions { 92 ulimits := make(map[string]*units.Ulimit) 93 return buildOptions{ 94 tags: opts.NewListOpts(validateTag), 95 buildArgs: opts.NewListOpts(opts.ValidateEnv), 96 ulimits: opts.NewUlimitOpt(&ulimits), 97 labels: opts.NewListOpts(opts.ValidateLabel), 98 extraHosts: opts.NewListOpts(opts.ValidateExtraHost), 99 } 100 } 101 102 // NewBuildCommand creates a new `docker build` command 103 func NewBuildCommand(dockerCli command.Cli) *cobra.Command { 104 options := newBuildOptions() 105 106 cmd := &cobra.Command{ 107 Use: "build [OPTIONS] PATH | URL | -", 108 Short: "Build an image from a Dockerfile", 109 Args: cli.ExactArgs(1), 110 RunE: func(cmd *cobra.Command, args []string) error { 111 options.context = args[0] 112 return runBuild(dockerCli, options) 113 }, 114 } 115 116 flags := cmd.Flags() 117 118 flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format") 119 flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") 120 flags.Var(options.ulimits, "ulimit", "Ulimit options") 121 flags.SetAnnotation("ulimit", "no-buildkit", nil) 122 flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") 123 flags.VarP(&options.memory, "memory", "m", "Memory limit") 124 flags.SetAnnotation("memory", "no-buildkit", nil) 125 flags.Var(&options.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") 126 flags.SetAnnotation("memory-swap", "no-buildkit", nil) 127 flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm") 128 flags.SetAnnotation("shm-size", "no-buildkit", nil) 129 flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") 130 flags.SetAnnotation("cpu-shares", "no-buildkit", nil) 131 flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") 132 flags.SetAnnotation("cpu-period", "no-buildkit", nil) 133 flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") 134 flags.SetAnnotation("cpu-quota", "no-buildkit", nil) 135 flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") 136 flags.SetAnnotation("cpuset-cpus", "no-buildkit", nil) 137 flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") 138 flags.SetAnnotation("cpuset-mems", "no-buildkit", nil) 139 flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") 140 flags.SetAnnotation("cgroup-parent", "no-buildkit", nil) 141 flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") 142 flags.Var(&options.labels, "label", "Set metadata for an image") 143 flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") 144 flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") 145 flags.SetAnnotation("rm", "no-buildkit", nil) 146 flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") 147 flags.SetAnnotation("force-rm", "no-buildkit", nil) 148 flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") 149 flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") 150 flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") 151 flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") 152 flags.SetAnnotation("compress", "no-buildkit", nil) 153 flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") 154 flags.SetAnnotation("security-opt", "no-buildkit", nil) 155 flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") 156 flags.SetAnnotation("network", "version", []string{"1.25"}) 157 flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") 158 flags.StringVar(&options.target, "target", "", "Set the target build stage to build.") 159 flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") 160 161 command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) 162 163 flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") 164 flags.SetAnnotation("platform", "version", []string{"1.38"}) 165 flags.SetAnnotation("platform", "buildkit", nil) 166 167 flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") 168 flags.SetAnnotation("squash", "experimental", nil) 169 flags.SetAnnotation("squash", "version", []string{"1.25"}) 170 171 flags.BoolVar(&options.stream, "stream", false, "Stream attaches to server to negotiate build context") 172 flags.MarkHidden("stream") 173 174 flags.StringVar(&options.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output") 175 flags.SetAnnotation("progress", "buildkit", nil) 176 177 flags.StringArrayVar(&options.secrets, "secret", []string{}, "Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret") 178 flags.SetAnnotation("secret", "version", []string{"1.39"}) 179 flags.SetAnnotation("secret", "buildkit", nil) 180 181 flags.StringArrayVar(&options.ssh, "ssh", []string{}, "SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|<id>[=<socket>|<key>[,<key>]])") 182 flags.SetAnnotation("ssh", "version", []string{"1.39"}) 183 flags.SetAnnotation("ssh", "buildkit", nil) 184 185 flags.StringArrayVarP(&options.outputs, "output", "o", []string{}, "Output destination (format: type=local,dest=path)") 186 flags.SetAnnotation("output", "version", []string{"1.40"}) 187 flags.SetAnnotation("output", "buildkit", nil) 188 189 return cmd 190 } 191 192 // lastProgressOutput is the same as progress.Output except 193 // that it only output with the last update. It is used in 194 // non terminal scenarios to suppress verbose messages 195 type lastProgressOutput struct { 196 output progress.Output 197 } 198 199 // WriteProgress formats progress information from a ProgressReader. 200 func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { 201 if !prog.LastUpdate { 202 return nil 203 } 204 205 return out.output.WriteProgress(prog) 206 } 207 208 // nolint: gocyclo 209 func runBuild(dockerCli command.Cli, options buildOptions) error { 210 buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo()) 211 if err != nil { 212 return err 213 } 214 if buildkitEnabled { 215 return runBuildBuildKit(dockerCli, options) 216 } 217 218 var ( 219 buildCtx io.ReadCloser 220 dockerfileCtx io.ReadCloser 221 contextDir string 222 tempDir string 223 relDockerfile string 224 progBuff io.Writer 225 buildBuff io.Writer 226 remote string 227 ) 228 229 if options.stream { 230 _, _ = fmt.Fprint(dockerCli.Err(), `DEPRECATED: The experimental --stream flag has been removed and the build context 231 will be sent non-streaming. Enable BuildKit instead with DOCKER_BUILDKIT=1 232 to stream build context, see https://docs.docker.com/go/buildkit/ 233 234 `) 235 } 236 237 if options.dockerfileFromStdin() { 238 if options.contextFromStdin() { 239 return errStdinConflict 240 } 241 dockerfileCtx = dockerCli.In() 242 } 243 244 specifiedContext := options.context 245 progBuff = dockerCli.Out() 246 buildBuff = dockerCli.Out() 247 if options.quiet { 248 progBuff = bytes.NewBuffer(nil) 249 buildBuff = bytes.NewBuffer(nil) 250 } 251 if options.imageIDFile != "" { 252 // Avoid leaving a stale file if we eventually fail 253 if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) { 254 return errors.Wrap(err, "Removing image ID file") 255 } 256 } 257 258 switch { 259 case options.contextFromStdin(): 260 // buildCtx is tar archive. if stdin was dockerfile then it is wrapped 261 buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) 262 case isLocalDir(specifiedContext): 263 contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) 264 if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { 265 // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx 266 dockerfileCtx, err = os.Open(options.dockerfileName) 267 if err != nil { 268 return errors.Errorf("unable to open Dockerfile: %v", err) 269 } 270 defer dockerfileCtx.Close() 271 } 272 case urlutil.IsGitURL(specifiedContext): 273 tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) 274 case urlutil.IsURL(specifiedContext): 275 buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) 276 default: 277 return errors.Errorf("unable to prepare context: path %q not found", specifiedContext) 278 } 279 280 if err != nil { 281 if options.quiet && urlutil.IsURL(specifiedContext) { 282 fmt.Fprintln(dockerCli.Err(), progBuff) 283 } 284 return errors.Errorf("unable to prepare context: %s", err) 285 } 286 287 if tempDir != "" { 288 defer os.RemoveAll(tempDir) 289 contextDir = tempDir 290 } 291 292 // read from a directory into tar archive 293 if buildCtx == nil { 294 excludes, err := build.ReadDockerignore(contextDir) 295 if err != nil { 296 return err 297 } 298 299 if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { 300 return errors.Errorf("error checking context: '%s'.", err) 301 } 302 303 // And canonicalize dockerfile name to a platform-independent one 304 relDockerfile = archive.CanonicalTarNameForPath(relDockerfile) 305 306 excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin()) 307 buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ 308 ExcludePatterns: excludes, 309 ChownOpts: &idtools.Identity{UID: 0, GID: 0}, 310 }) 311 if err != nil { 312 return err 313 } 314 } 315 316 // replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context 317 if dockerfileCtx != nil && buildCtx != nil { 318 buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx) 319 if err != nil { 320 return err 321 } 322 } 323 324 ctx, cancel := context.WithCancel(context.Background()) 325 defer cancel() 326 327 var resolvedTags []*resolvedTag 328 if !options.untrusted { 329 translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { 330 return TrustedReference(ctx, dockerCli, ref, nil) 331 } 332 // if there is a tar wrapper, the dockerfile needs to be replaced inside it 333 if buildCtx != nil { 334 // Wrap the tar archive to replace the Dockerfile entry with the rewritten 335 // Dockerfile which uses trusted pulls. 336 buildCtx = replaceDockerfileForContentTrust(ctx, buildCtx, relDockerfile, translator, &resolvedTags) 337 } else if dockerfileCtx != nil { 338 // if there was not archive context still do the possible replacements in Dockerfile 339 newDockerfile, _, err := rewriteDockerfileFromForContentTrust(ctx, dockerfileCtx, translator) 340 if err != nil { 341 return err 342 } 343 dockerfileCtx = ioutil.NopCloser(bytes.NewBuffer(newDockerfile)) 344 } 345 } 346 347 if options.compress { 348 buildCtx, err = build.Compress(buildCtx) 349 if err != nil { 350 return err 351 } 352 } 353 354 // Setup an upload progress bar 355 progressOutput := streamformatter.NewProgressOutput(progBuff) 356 if !dockerCli.Out().IsTerminal() { 357 progressOutput = &lastProgressOutput{output: progressOutput} 358 } 359 360 // if up to this point nothing has set the context then we must have another 361 // way for sending it(streaming) and set the context to the Dockerfile 362 if dockerfileCtx != nil && buildCtx == nil { 363 buildCtx = dockerfileCtx 364 } 365 366 var body io.Reader 367 if buildCtx != nil { 368 body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") 369 } 370 371 configFile := dockerCli.ConfigFile() 372 creds, _ := configFile.GetAllCredentials() 373 authConfigs := make(map[string]types.AuthConfig, len(creds)) 374 for k, auth := range creds { 375 authConfigs[k] = types.AuthConfig(auth) 376 } 377 buildOptions := imageBuildOptions(dockerCli, options) 378 buildOptions.Version = types.BuilderV1 379 buildOptions.Dockerfile = relDockerfile 380 buildOptions.AuthConfigs = authConfigs 381 buildOptions.RemoteContext = remote 382 383 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) 384 if err != nil { 385 if options.quiet { 386 fmt.Fprintf(dockerCli.Err(), "%s", progBuff) 387 } 388 cancel() 389 return err 390 } 391 defer response.Body.Close() 392 393 imageID := "" 394 aux := func(msg jsonmessage.JSONMessage) { 395 var result types.BuildResult 396 if err := json.Unmarshal(*msg.Aux, &result); err != nil { 397 fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err) 398 } else { 399 imageID = result.ID 400 } 401 } 402 403 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), aux) 404 if err != nil { 405 if jerr, ok := err.(*jsonmessage.JSONError); ok { 406 // If no error code is set, default to 1 407 if jerr.Code == 0 { 408 jerr.Code = 1 409 } 410 if options.quiet { 411 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) 412 } 413 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} 414 } 415 return err 416 } 417 418 // Windows: show error message about modified file permissions if the 419 // daemon isn't running Windows. 420 if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { 421 fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+ 422 "image from Windows against a non-Windows Docker host. All files and "+ 423 "directories added to build context will have '-rwxr-xr-x' permissions. "+ 424 "It is recommended to double check and reset permissions for sensitive "+ 425 "files and directories.") 426 } 427 428 // Everything worked so if -q was provided the output from the daemon 429 // should be just the image ID and we'll print that to stdout. 430 if options.quiet { 431 imageID = fmt.Sprintf("%s", buildBuff) 432 _, _ = fmt.Fprint(dockerCli.Out(), imageID) 433 } 434 435 if options.imageIDFile != "" { 436 if imageID == "" { 437 return errors.Errorf("Server did not provide an image ID. Cannot write %s", options.imageIDFile) 438 } 439 if err := ioutil.WriteFile(options.imageIDFile, []byte(imageID), 0666); err != nil { 440 return err 441 } 442 } 443 if !options.untrusted { 444 // Since the build was successful, now we must tag any of the resolved 445 // images from the above Dockerfile rewrite. 446 for _, resolved := range resolvedTags { 447 if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { 448 return err 449 } 450 } 451 } 452 453 return nil 454 } 455 456 func isLocalDir(c string) bool { 457 _, err := os.Stat(c) 458 return err == nil 459 } 460 461 type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) 462 463 // validateTag checks if the given image name can be resolved. 464 func validateTag(rawRepo string) (string, error) { 465 _, err := reference.ParseNormalizedNamed(rawRepo) 466 if err != nil { 467 return "", err 468 } 469 470 return rawRepo, nil 471 } 472 473 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`) 474 475 // resolvedTag records the repository, tag, and resolved digest reference 476 // from a Dockerfile rewrite. 477 type resolvedTag struct { 478 digestRef reference.Canonical 479 tagRef reference.NamedTagged 480 } 481 482 // rewriteDockerfileFromForContentTrust rewrites the given Dockerfile by resolving images in 483 // "FROM <image>" instructions to a digest reference. `translator` is a 484 // function that takes a repository name and tag reference and returns a 485 // trusted digest reference. 486 // This should be called *only* when content trust is enabled 487 func rewriteDockerfileFromForContentTrust(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { 488 scanner := bufio.NewScanner(dockerfile) 489 buf := bytes.NewBuffer(nil) 490 491 // Scan the lines of the Dockerfile, looking for a "FROM" line. 492 for scanner.Scan() { 493 line := scanner.Text() 494 495 matches := dockerfileFromLinePattern.FindStringSubmatch(line) 496 if matches != nil && matches[1] != api.NoBaseImageSpecifier { 497 // Replace the line with a resolved "FROM repo@digest" 498 var ref reference.Named 499 ref, err = reference.ParseNormalizedNamed(matches[1]) 500 if err != nil { 501 return nil, nil, err 502 } 503 ref = reference.TagNameOnly(ref) 504 if ref, ok := ref.(reference.NamedTagged); ok { 505 trustedRef, err := translator(ctx, ref) 506 if err != nil { 507 return nil, nil, err 508 } 509 510 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", reference.FamiliarString(trustedRef))) 511 resolvedTags = append(resolvedTags, &resolvedTag{ 512 digestRef: trustedRef, 513 tagRef: ref, 514 }) 515 } 516 } 517 518 _, err := fmt.Fprintln(buf, line) 519 if err != nil { 520 return nil, nil, err 521 } 522 } 523 524 return buf.Bytes(), resolvedTags, scanner.Err() 525 } 526 527 // replaceDockerfileForContentTrust wraps the given input tar archive stream and 528 // uses the translator to replace the Dockerfile which uses a trusted reference. 529 // Returns a new tar archive stream with the replaced Dockerfile. 530 func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { 531 pipeReader, pipeWriter := io.Pipe() 532 go func() { 533 tarReader := tar.NewReader(inputTarStream) 534 tarWriter := tar.NewWriter(pipeWriter) 535 536 defer inputTarStream.Close() 537 538 for { 539 hdr, err := tarReader.Next() 540 if err == io.EOF { 541 // Signals end of archive. 542 tarWriter.Close() 543 pipeWriter.Close() 544 return 545 } 546 if err != nil { 547 pipeWriter.CloseWithError(err) 548 return 549 } 550 551 content := io.Reader(tarReader) 552 if hdr.Name == dockerfileName { 553 // This entry is the Dockerfile. Since the tar archive was 554 // generated from a directory on the local filesystem, the 555 // Dockerfile will only appear once in the archive. 556 var newDockerfile []byte 557 newDockerfile, *resolvedTags, err = rewriteDockerfileFromForContentTrust(ctx, content, translator) 558 if err != nil { 559 pipeWriter.CloseWithError(err) 560 return 561 } 562 hdr.Size = int64(len(newDockerfile)) 563 content = bytes.NewBuffer(newDockerfile) 564 } 565 566 if err := tarWriter.WriteHeader(hdr); err != nil { 567 pipeWriter.CloseWithError(err) 568 return 569 } 570 571 if _, err := io.Copy(tarWriter, content); err != nil { 572 pipeWriter.CloseWithError(err) 573 return 574 } 575 } 576 }() 577 578 return pipeReader 579 } 580 581 func imageBuildOptions(dockerCli command.Cli, options buildOptions) types.ImageBuildOptions { 582 configFile := dockerCli.ConfigFile() 583 return types.ImageBuildOptions{ 584 Memory: options.memory.Value(), 585 MemorySwap: options.memorySwap.Value(), 586 Tags: options.tags.GetAll(), 587 SuppressOutput: options.quiet, 588 NoCache: options.noCache, 589 Remove: options.rm, 590 ForceRemove: options.forceRm, 591 PullParent: options.pull, 592 Isolation: container.Isolation(options.isolation), 593 CPUSetCPUs: options.cpuSetCpus, 594 CPUSetMems: options.cpuSetMems, 595 CPUShares: options.cpuShares, 596 CPUQuota: options.cpuQuota, 597 CPUPeriod: options.cpuPeriod, 598 CgroupParent: options.cgroupParent, 599 ShmSize: options.shmSize.Value(), 600 Ulimits: options.ulimits.GetList(), 601 BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll())), 602 Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()), 603 CacheFrom: options.cacheFrom, 604 SecurityOpt: options.securityOpt, 605 NetworkMode: options.networkMode, 606 Squash: options.squash, 607 ExtraHosts: options.extraHosts.GetAll(), 608 Target: options.target, 609 Platform: options.platform, 610 } 611 } 612 613 func parseOutputs(inp []string) ([]types.ImageBuildOutput, error) { 614 var outs []types.ImageBuildOutput 615 if len(inp) == 0 { 616 return nil, nil 617 } 618 for _, s := range inp { 619 csvReader := csv.NewReader(strings.NewReader(s)) 620 fields, err := csvReader.Read() 621 if err != nil { 622 return nil, err 623 } 624 if len(fields) == 1 && fields[0] == s && !strings.HasPrefix(s, "type=") { 625 if s == "-" { 626 outs = append(outs, types.ImageBuildOutput{ 627 Type: "tar", 628 Attrs: map[string]string{ 629 "dest": s, 630 }, 631 }) 632 } else { 633 outs = append(outs, types.ImageBuildOutput{ 634 Type: "local", 635 Attrs: map[string]string{ 636 "dest": s, 637 }, 638 }) 639 } 640 continue 641 } 642 643 out := types.ImageBuildOutput{ 644 Attrs: map[string]string{}, 645 } 646 for _, field := range fields { 647 parts := strings.SplitN(field, "=", 2) 648 if len(parts) != 2 { 649 return nil, errors.Errorf("invalid value %s", field) 650 } 651 key := strings.ToLower(parts[0]) 652 value := parts[1] 653 switch key { 654 case "type": 655 out.Type = value 656 default: 657 out.Attrs[key] = value 658 } 659 } 660 if out.Type == "" { 661 return nil, errors.Errorf("type is required for output") 662 } 663 outs = append(outs, out) 664 } 665 return outs, nil 666 }