github.com/rumpl/bof@v23.0.0-rc.2+incompatible/builder/dockerfile/dispatchers.go (about) 1 package dockerfile // import "github.com/docker/docker/builder/dockerfile" 2 3 // This file contains the dispatchers for each command. Note that 4 // `nullDispatch` is not actually a command, but support for commands we parse 5 // but do nothing with. 6 // 7 // See evaluator.go for a higher level discussion of the whole evaluator 8 // package. 9 10 import ( 11 "bytes" 12 "fmt" 13 "runtime" 14 "sort" 15 "strings" 16 17 "github.com/containerd/containerd/platforms" 18 "github.com/docker/docker/api" 19 "github.com/docker/docker/api/types/strslice" 20 "github.com/docker/docker/builder" 21 "github.com/docker/docker/errdefs" 22 "github.com/docker/docker/image" 23 "github.com/docker/docker/pkg/jsonmessage" 24 "github.com/docker/docker/pkg/system" 25 "github.com/docker/go-connections/nat" 26 "github.com/moby/buildkit/frontend/dockerfile/instructions" 27 "github.com/moby/buildkit/frontend/dockerfile/parser" 28 "github.com/moby/buildkit/frontend/dockerfile/shell" 29 "github.com/moby/sys/signal" 30 specs "github.com/opencontainers/image-spec/specs-go/v1" 31 "github.com/pkg/errors" 32 ) 33 34 // ENV foo bar 35 // 36 // Sets the environment variable foo to bar, also makes interpolation 37 // in the dockerfile available from the next statement on via ${foo}. 38 func dispatchEnv(d dispatchRequest, c *instructions.EnvCommand) error { 39 runConfig := d.state.runConfig 40 commitMessage := bytes.NewBufferString("ENV") 41 for _, e := range c.Env { 42 name := e.Key 43 newVar := e.String() 44 45 commitMessage.WriteString(" " + newVar) 46 gotOne := false 47 for i, envVar := range runConfig.Env { 48 envParts := strings.SplitN(envVar, "=", 2) 49 compareFrom := envParts[0] 50 if shell.EqualEnvKeys(compareFrom, name) { 51 runConfig.Env[i] = newVar 52 gotOne = true 53 break 54 } 55 } 56 if !gotOne { 57 runConfig.Env = append(runConfig.Env, newVar) 58 } 59 } 60 return d.builder.commit(d.state, commitMessage.String()) 61 } 62 63 // MAINTAINER some text <maybe@an.email.address> 64 // 65 // Sets the maintainer metadata. 66 func dispatchMaintainer(d dispatchRequest, c *instructions.MaintainerCommand) error { 67 d.state.maintainer = c.Maintainer 68 return d.builder.commit(d.state, "MAINTAINER "+c.Maintainer) 69 } 70 71 // LABEL some json data describing the image 72 // 73 // Sets the Label variable foo to bar, 74 func dispatchLabel(d dispatchRequest, c *instructions.LabelCommand) error { 75 if d.state.runConfig.Labels == nil { 76 d.state.runConfig.Labels = make(map[string]string) 77 } 78 commitStr := "LABEL" 79 for _, v := range c.Labels { 80 d.state.runConfig.Labels[v.Key] = v.Value 81 commitStr += " " + v.String() 82 } 83 return d.builder.commit(d.state, commitStr) 84 } 85 86 // ADD foo /path 87 // 88 // Add the file 'foo' to '/path'. Tarball and Remote URL (http, https) handling 89 // exist here. If you do not wish to have this automatic handling, use COPY. 90 func dispatchAdd(d dispatchRequest, c *instructions.AddCommand) error { 91 if c.Chmod != "" { 92 return errors.New("the --chmod option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled") 93 } 94 downloader := newRemoteSourceDownloader(d.builder.Output, d.builder.Stdout) 95 copier := copierFromDispatchRequest(d, downloader, nil) 96 defer copier.Cleanup() 97 98 copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "ADD") 99 if err != nil { 100 return err 101 } 102 copyInstruction.chownStr = c.Chown 103 copyInstruction.allowLocalDecompression = true 104 105 return d.builder.performCopy(d, copyInstruction) 106 } 107 108 // COPY foo /path 109 // 110 // Same as 'ADD' but without the tar and remote url handling. 111 func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error { 112 if c.Chmod != "" { 113 return errors.New("the --chmod option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled") 114 } 115 var im *imageMount 116 var err error 117 if c.From != "" { 118 im, err = d.getImageMount(c.From) 119 if err != nil { 120 return errors.Wrapf(err, "invalid from flag value %s", c.From) 121 } 122 } 123 copier := copierFromDispatchRequest(d, errOnSourceDownload, im) 124 defer copier.Cleanup() 125 copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "COPY") 126 if err != nil { 127 return err 128 } 129 copyInstruction.chownStr = c.Chown 130 if c.From != "" && copyInstruction.chownStr == "" { 131 copyInstruction.preserveOwnership = true 132 } 133 return d.builder.performCopy(d, copyInstruction) 134 } 135 136 func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error) { 137 if imageRefOrID == "" { 138 // TODO: this could return the source in the default case as well? 139 return nil, nil 140 } 141 142 var localOnly bool 143 stage, err := d.stages.get(imageRefOrID) 144 if err != nil { 145 return nil, err 146 } 147 if stage != nil { 148 imageRefOrID = stage.Image 149 localOnly = true 150 } 151 return d.builder.imageSources.Get(imageRefOrID, localOnly, d.builder.platform) 152 } 153 154 // FROM [--platform=platform] imagename[:tag | @digest] [AS build-stage-name] 155 func initializeStage(d dispatchRequest, cmd *instructions.Stage) error { 156 d.builder.imageProber.Reset() 157 158 var platform *specs.Platform 159 if v := cmd.Platform; v != "" { 160 v, err := d.getExpandedString(d.shlex, v) 161 if err != nil { 162 return errors.Wrapf(err, "failed to process arguments for platform %s", v) 163 } 164 165 p, err := platforms.Parse(v) 166 if err != nil { 167 return errors.Wrapf(err, "failed to parse platform %s", v) 168 } 169 platform = &p 170 } 171 172 image, err := d.getFromImage(d.shlex, cmd.BaseName, platform) 173 if err != nil { 174 return err 175 } 176 state := d.state 177 if err := state.beginStage(cmd.Name, image); err != nil { 178 return err 179 } 180 if len(state.runConfig.OnBuild) > 0 { 181 triggers := state.runConfig.OnBuild 182 state.runConfig.OnBuild = nil 183 return dispatchTriggeredOnBuild(d, triggers) 184 } 185 return nil 186 } 187 188 func dispatchTriggeredOnBuild(d dispatchRequest, triggers []string) error { 189 fmt.Fprintf(d.builder.Stdout, "# Executing %d build trigger", len(triggers)) 190 if len(triggers) > 1 { 191 fmt.Fprint(d.builder.Stdout, "s") 192 } 193 fmt.Fprintln(d.builder.Stdout) 194 for _, trigger := range triggers { 195 d.state.updateRunConfig() 196 ast, err := parser.Parse(strings.NewReader(trigger)) 197 if err != nil { 198 return err 199 } 200 if len(ast.AST.Children) != 1 { 201 return errors.New("onbuild trigger should be a single expression") 202 } 203 cmd, err := instructions.ParseCommand(ast.AST.Children[0]) 204 if err != nil { 205 var uiErr *instructions.UnknownInstructionError 206 if errors.As(err, &uiErr) { 207 buildsFailed.WithValues(metricsUnknownInstructionError).Inc() 208 } 209 return err 210 } 211 err = dispatch(d, cmd) 212 if err != nil { 213 return err 214 } 215 } 216 return nil 217 } 218 219 func (d *dispatchRequest) getExpandedString(shlex *shell.Lex, str string) (string, error) { 220 substitutionArgs := []string{} 221 for key, value := range d.state.buildArgs.GetAllMeta() { 222 substitutionArgs = append(substitutionArgs, key+"="+value) 223 } 224 225 name, err := shlex.ProcessWord(str, substitutionArgs) 226 if err != nil { 227 return "", err 228 } 229 return name, nil 230 } 231 232 func (d *dispatchRequest) getImageOrStage(name string, platform *specs.Platform) (builder.Image, error) { 233 var localOnly bool 234 if im, ok := d.stages.getByName(name); ok { 235 name = im.Image 236 localOnly = true 237 } 238 239 if platform == nil { 240 platform = d.builder.platform 241 } 242 243 // Windows cannot support a container with no base image. 244 if name == api.NoBaseImageSpecifier { 245 // Windows supports scratch. What is not supported is running containers from it. 246 if runtime.GOOS == "windows" { 247 return nil, errors.New("Windows does not support FROM scratch") 248 } 249 250 // TODO: scratch should not have an os. It should be nil image. 251 imageImage := &image.Image{} 252 if platform != nil { 253 imageImage.OS = platform.OS 254 } else { 255 imageImage.OS = runtime.GOOS 256 } 257 return builder.Image(imageImage), nil 258 } 259 imageMount, err := d.builder.imageSources.Get(name, localOnly, platform) 260 if err != nil { 261 return nil, err 262 } 263 return imageMount.Image(), nil 264 } 265 func (d *dispatchRequest) getFromImage(shlex *shell.Lex, basename string, platform *specs.Platform) (builder.Image, error) { 266 name, err := d.getExpandedString(shlex, basename) 267 if err != nil { 268 return nil, err 269 } 270 // Empty string is interpreted to FROM scratch by images.GetImageAndReleasableLayer, 271 // so validate expanded result is not empty. 272 if name == "" { 273 return nil, errors.Errorf("base name (%s) should not be blank", basename) 274 } 275 276 return d.getImageOrStage(name, platform) 277 } 278 279 func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { 280 d.state.runConfig.OnBuild = append(d.state.runConfig.OnBuild, c.Expression) 281 return d.builder.commit(d.state, "ONBUILD "+c.Expression) 282 } 283 284 // WORKDIR /tmp 285 // 286 // Set the working directory for future RUN/CMD/etc statements. 287 func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { 288 runConfig := d.state.runConfig 289 var err error 290 runConfig.WorkingDir, err = normalizeWorkdir(d.state.operatingSystem, runConfig.WorkingDir, c.Path) 291 if err != nil { 292 return err 293 } 294 295 // For performance reasons, we explicitly do a create/mkdir now 296 // This avoids having an unnecessary expensive mount/unmount calls 297 // (on Windows in particular) during each container create. 298 // Prior to 1.13, the mkdir was deferred and not executed at this step. 299 if d.builder.disableCommit { 300 // Don't call back into the daemon if we're going through docker commit --change "WORKDIR /foo". 301 // We've already updated the runConfig and that's enough. 302 return nil 303 } 304 305 comment := "WORKDIR " + runConfig.WorkingDir 306 runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.state.operatingSystem)) 307 308 containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) 309 if err != nil || containerID == "" { 310 return err 311 } 312 313 if err := d.builder.docker.ContainerCreateWorkdir(containerID); err != nil { 314 return err 315 } 316 317 return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd) 318 } 319 320 // RUN some command yo 321 // 322 // run a command and commit the image. Args are automatically prepended with 323 // the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under 324 // Windows, in the event there is only one argument The difference in processing: 325 // 326 // RUN echo hi # sh -c echo hi (Linux and LCOW) 327 // RUN echo hi # cmd /S /C echo hi (Windows) 328 // RUN [ "echo", "hi" ] # echo hi 329 func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error { 330 if !system.IsOSSupported(d.state.operatingSystem) { 331 return system.ErrNotSupportedOperatingSystem 332 } 333 334 if len(c.FlagsUsed) > 0 { 335 // classic builder RUN currently does not support any flags, so fail on the first one 336 return errors.Errorf("the --%s option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled", c.FlagsUsed[0]) 337 } 338 339 stateRunConfig := d.state.runConfig 340 cmdFromArgs, argsEscaped := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.state.operatingSystem, c.Name(), c.String()) 341 buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env) 342 343 saveCmd := cmdFromArgs 344 if len(buildArgs) > 0 { 345 saveCmd = prependEnvOnCmd(d.state.buildArgs, buildArgs, cmdFromArgs) 346 } 347 348 runConfigForCacheProbe := copyRunConfig(stateRunConfig, 349 withCmd(saveCmd), 350 withArgsEscaped(argsEscaped), 351 withEntrypointOverride(saveCmd, nil)) 352 if hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe); err != nil || hit { 353 return err 354 } 355 356 runConfig := copyRunConfig(stateRunConfig, 357 withCmd(cmdFromArgs), 358 withArgsEscaped(argsEscaped), 359 withEnv(append(stateRunConfig.Env, buildArgs...)), 360 withEntrypointOverride(saveCmd, strslice.StrSlice{""}), 361 withoutHealthcheck()) 362 363 cID, err := d.builder.create(runConfig) 364 if err != nil { 365 return err 366 } 367 368 if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil { 369 if err, ok := err.(*statusCodeError); ok { 370 // TODO: change error type, because jsonmessage.JSONError assumes HTTP 371 msg := fmt.Sprintf( 372 "The command '%s' returned a non-zero code: %d", 373 strings.Join(runConfig.Cmd, " "), err.StatusCode()) 374 if err.Error() != "" { 375 msg = fmt.Sprintf("%s: %s", msg, err.Error()) 376 } 377 return &jsonmessage.JSONError{ 378 Message: msg, 379 Code: err.StatusCode(), 380 } 381 } 382 return err 383 } 384 385 // Don't persist the argsEscaped value in the committed image. Use the original 386 // from previous build steps (only CMD and ENTRYPOINT persist this). 387 if d.state.operatingSystem == "windows" { 388 runConfigForCacheProbe.ArgsEscaped = stateRunConfig.ArgsEscaped 389 } 390 391 return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe) 392 } 393 394 // Derive the command to use for probeCache() and to commit in this container. 395 // Note that we only do this if there are any build-time env vars. Also, we 396 // use the special argument "|#" at the start of the args array. This will 397 // avoid conflicts with any RUN command since commands can not 398 // start with | (vertical bar). The "#" (number of build envs) is there to 399 // help ensure proper cache matches. We don't want a RUN command 400 // that starts with "foo=abc" to be considered part of a build-time env var. 401 // 402 // remove any unreferenced built-in args from the environment variables. 403 // These args are transparent so resulting image should be the same regardless 404 // of the value. 405 func prependEnvOnCmd(buildArgs *BuildArgs, buildArgVars []string, cmd strslice.StrSlice) strslice.StrSlice { 406 var tmpBuildEnv []string 407 for _, env := range buildArgVars { 408 key := strings.SplitN(env, "=", 2)[0] 409 if buildArgs.IsReferencedOrNotBuiltin(key) { 410 tmpBuildEnv = append(tmpBuildEnv, env) 411 } 412 } 413 414 sort.Strings(tmpBuildEnv) 415 tmpEnv := append([]string{fmt.Sprintf("|%d", len(tmpBuildEnv))}, tmpBuildEnv...) 416 return strslice.StrSlice(append(tmpEnv, cmd...)) 417 } 418 419 // CMD foo 420 // 421 // Set the default command to run in the container (which may be empty). 422 // Argument handling is the same as RUN. 423 func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error { 424 runConfig := d.state.runConfig 425 cmd, argsEscaped := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem, c.Name(), c.String()) 426 427 // We warn here as Windows shell processing operates differently to Linux. 428 // Linux: /bin/sh -c "echo hello" world --> hello 429 // Windows: cmd /s /c "echo hello" world --> hello world 430 if d.state.operatingSystem == "windows" && 431 len(runConfig.Entrypoint) > 0 && 432 d.state.runConfig.ArgsEscaped != argsEscaped { 433 fmt.Fprintf(d.builder.Stderr, " ---> [Warning] Shell-form ENTRYPOINT and exec-form CMD may have unexpected results\n") 434 } 435 436 runConfig.Cmd = cmd 437 runConfig.ArgsEscaped = argsEscaped 438 439 if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil { 440 return err 441 } 442 if len(c.ShellDependantCmdLine.CmdLine) != 0 { 443 d.state.cmdSet = true 444 } 445 446 return nil 447 } 448 449 // HEALTHCHECK foo 450 // 451 // Set the default healthcheck command to run in the container (which may be empty). 452 // Argument handling is the same as RUN. 453 func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) error { 454 runConfig := d.state.runConfig 455 if runConfig.Healthcheck != nil { 456 oldCmd := runConfig.Healthcheck.Test 457 if len(oldCmd) > 0 && oldCmd[0] != "NONE" { 458 fmt.Fprintf(d.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) 459 } 460 } 461 runConfig.Healthcheck = c.Health 462 return d.builder.commit(d.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck)) 463 } 464 465 // ENTRYPOINT /usr/sbin/nginx 466 // 467 // Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments 468 // to /usr/sbin/nginx. Uses the default shell if not in JSON format. 469 // 470 // Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint 471 // is initialized at newBuilder time instead of through argument parsing. 472 func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error { 473 runConfig := d.state.runConfig 474 cmd, argsEscaped := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem, c.Name(), c.String()) 475 476 // This warning is a little more complex than in dispatchCmd(), as the Windows base images (similar 477 // universally to almost every Linux image out there) have a single .Cmd field populated so that 478 // `docker run --rm image` starts the default shell which would typically be sh on Linux, 479 // or cmd on Windows. The catch to this is that if a dockerfile had `CMD ["c:\\windows\\system32\\cmd.exe"]`, 480 // we wouldn't be able to tell the difference. However, that would be highly unlikely, and besides, this 481 // is only trying to give a helpful warning of possibly unexpected results. 482 if d.state.operatingSystem == "windows" && 483 d.state.runConfig.ArgsEscaped != argsEscaped && 484 ((len(runConfig.Cmd) == 1 && strings.ToLower(runConfig.Cmd[0]) != `c:\windows\system32\cmd.exe` && len(runConfig.Shell) == 0) || (len(runConfig.Cmd) > 1)) { 485 fmt.Fprintf(d.builder.Stderr, " ---> [Warning] Shell-form CMD and exec-form ENTRYPOINT may have unexpected results\n") 486 } 487 488 runConfig.Entrypoint = cmd 489 runConfig.ArgsEscaped = argsEscaped 490 if !d.state.cmdSet { 491 runConfig.Cmd = nil 492 } 493 494 return d.builder.commit(d.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint)) 495 } 496 497 // EXPOSE 6667/tcp 7000/tcp 498 // 499 // Expose ports for links and port mappings. This all ends up in 500 // req.runConfig.ExposedPorts for runconfig. 501 func dispatchExpose(d dispatchRequest, c *instructions.ExposeCommand, envs []string) error { 502 // custom multi word expansion 503 // expose $FOO with FOO="80 443" is expanded as EXPOSE [80,443]. This is the only command supporting word to words expansion 504 // so the word processing has been de-generalized 505 ports := []string{} 506 for _, p := range c.Ports { 507 ps, err := d.shlex.ProcessWords(p, envs) 508 if err != nil { 509 return err 510 } 511 ports = append(ports, ps...) 512 } 513 c.Ports = ports 514 515 ps, _, err := nat.ParsePortSpecs(ports) 516 if err != nil { 517 return err 518 } 519 520 if d.state.runConfig.ExposedPorts == nil { 521 d.state.runConfig.ExposedPorts = make(nat.PortSet) 522 } 523 for p := range ps { 524 d.state.runConfig.ExposedPorts[p] = struct{}{} 525 } 526 527 return d.builder.commit(d.state, "EXPOSE "+strings.Join(c.Ports, " ")) 528 } 529 530 // USER foo 531 // 532 // Set the user to 'foo' for future commands and when running the 533 // ENTRYPOINT/CMD at container run time. 534 func dispatchUser(d dispatchRequest, c *instructions.UserCommand) error { 535 d.state.runConfig.User = c.User 536 return d.builder.commit(d.state, fmt.Sprintf("USER %v", c.User)) 537 } 538 539 // VOLUME /foo 540 // 541 // Expose the volume /foo for use. Will also accept the JSON array form. 542 func dispatchVolume(d dispatchRequest, c *instructions.VolumeCommand) error { 543 if d.state.runConfig.Volumes == nil { 544 d.state.runConfig.Volumes = map[string]struct{}{} 545 } 546 for _, v := range c.Volumes { 547 if v == "" { 548 return errors.New("VOLUME specified can not be an empty string") 549 } 550 d.state.runConfig.Volumes[v] = struct{}{} 551 } 552 return d.builder.commit(d.state, fmt.Sprintf("VOLUME %v", c.Volumes)) 553 } 554 555 // STOPSIGNAL signal 556 // 557 // Set the signal that will be used to kill the container. 558 func dispatchStopSignal(d dispatchRequest, c *instructions.StopSignalCommand) error { 559 _, err := signal.ParseSignal(c.Signal) 560 if err != nil { 561 return errdefs.InvalidParameter(err) 562 } 563 d.state.runConfig.StopSignal = c.Signal 564 return d.builder.commit(d.state, fmt.Sprintf("STOPSIGNAL %v", c.Signal)) 565 } 566 567 // ARG name[=value] 568 // 569 // Adds the variable foo to the trusted list of variables that can be passed 570 // to builder using the --build-arg flag for expansion/substitution or passing to 'run'. 571 // Dockerfile author may optionally set a default value of this variable. 572 func dispatchArg(d dispatchRequest, c *instructions.ArgCommand) error { 573 var commitStr strings.Builder 574 commitStr.WriteString("ARG ") 575 for i, arg := range c.Args { 576 if i > 0 { 577 commitStr.WriteString(" ") 578 } 579 commitStr.WriteString(arg.Key) 580 if arg.Value != nil { 581 commitStr.WriteString("=") 582 commitStr.WriteString(*arg.Value) 583 } 584 d.state.buildArgs.AddArg(arg.Key, arg.Value) 585 } 586 587 return d.builder.commit(d.state, commitStr.String()) 588 } 589 590 // SHELL powershell -command 591 // 592 // Set the non-default shell to use. 593 func dispatchShell(d dispatchRequest, c *instructions.ShellCommand) error { 594 d.state.runConfig.Shell = c.Shell 595 return d.builder.commit(d.state, fmt.Sprintf("SHELL %v", d.state.runConfig.Shell)) 596 }