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