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