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