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