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