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