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