github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/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/container" 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/signal" 26 "github.com/docker/docker/pkg/system" 27 "github.com/docker/go-connections/nat" 28 "github.com/moby/buildkit/frontend/dockerfile/instructions" 29 "github.com/moby/buildkit/frontend/dockerfile/parser" 30 "github.com/moby/buildkit/frontend/dockerfile/shell" 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 // 40 func dispatchEnv(d dispatchRequest, c *instructions.EnvCommand) error { 41 runConfig := d.state.runConfig 42 commitMessage := bytes.NewBufferString("ENV") 43 for _, e := range c.Env { 44 name := e.Key 45 newVar := e.String() 46 47 commitMessage.WriteString(" " + newVar) 48 gotOne := false 49 for i, envVar := range runConfig.Env { 50 envParts := strings.SplitN(envVar, "=", 2) 51 compareFrom := envParts[0] 52 if shell.EqualEnvKeys(compareFrom, name) { 53 runConfig.Env[i] = newVar 54 gotOne = true 55 break 56 } 57 } 58 if !gotOne { 59 runConfig.Env = append(runConfig.Env, newVar) 60 } 61 } 62 return d.builder.commit(d.state, commitMessage.String()) 63 } 64 65 // MAINTAINER some text <maybe@an.email.address> 66 // 67 // Sets the maintainer metadata. 68 func dispatchMaintainer(d dispatchRequest, c *instructions.MaintainerCommand) error { 69 70 d.state.maintainer = c.Maintainer 71 return d.builder.commit(d.state, "MAINTAINER "+c.Maintainer) 72 } 73 74 // LABEL some json data describing the image 75 // 76 // Sets the Label variable foo to bar, 77 // 78 func dispatchLabel(d dispatchRequest, c *instructions.LabelCommand) error { 79 if d.state.runConfig.Labels == nil { 80 d.state.runConfig.Labels = make(map[string]string) 81 } 82 commitStr := "LABEL" 83 for _, v := range c.Labels { 84 d.state.runConfig.Labels[v.Key] = v.Value 85 commitStr += " " + v.String() 86 } 87 return d.builder.commit(d.state, commitStr) 88 } 89 90 // ADD foo /path 91 // 92 // Add the file 'foo' to '/path'. Tarball and Remote URL (http, https) handling 93 // exist here. If you do not wish to have this automatic handling, use COPY. 94 // 95 func dispatchAdd(d dispatchRequest, c *instructions.AddCommand) error { 96 downloader := newRemoteSourceDownloader(d.builder.Output, d.builder.Stdout) 97 copier := copierFromDispatchRequest(d, downloader, nil) 98 defer copier.Cleanup() 99 100 copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "ADD") 101 if err != nil { 102 return err 103 } 104 copyInstruction.chownStr = c.Chown 105 copyInstruction.allowLocalDecompression = true 106 107 return d.builder.performCopy(d, copyInstruction) 108 } 109 110 // COPY foo /path 111 // 112 // Same as 'ADD' but without the tar and remote url handling. 113 // 114 func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error { 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 131 return d.builder.performCopy(d, copyInstruction) 132 } 133 134 func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error) { 135 if imageRefOrID == "" { 136 // TODO: this could return the source in the default case as well? 137 return nil, nil 138 } 139 140 var localOnly bool 141 stage, err := d.stages.get(imageRefOrID) 142 if err != nil { 143 return nil, err 144 } 145 if stage != nil { 146 imageRefOrID = stage.Image 147 localOnly = true 148 } 149 return d.builder.imageSources.Get(imageRefOrID, localOnly, d.builder.platform) 150 } 151 152 // FROM [--platform=platform] imagename[:tag | @digest] [AS build-stage-name] 153 // 154 func initializeStage(d dispatchRequest, cmd *instructions.Stage) error { 155 d.builder.imageProber.Reset() 156 157 var platform *specs.Platform 158 if v := cmd.Platform; v != "" { 159 v, err := d.getExpandedString(d.shlex, v) 160 if err != nil { 161 return errors.Wrapf(err, "failed to process arguments for platform %s", v) 162 } 163 164 p, err := platforms.Parse(v) 165 if err != nil { 166 return errors.Wrapf(err, "failed to parse platform %s", v) 167 } 168 if err := system.ValidatePlatform(p); err != nil { 169 return err 170 } 171 platform = &p 172 } 173 174 image, err := d.getFromImage(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(d, triggers) 186 } 187 return nil 188 } 189 190 func dispatchTriggeredOnBuild(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 if instructions.IsUnknownInstruction(err) { 208 buildsFailed.WithValues(metricsUnknownInstructionError).Inc() 209 } 210 return err 211 } 212 err = dispatch(d, cmd) 213 if err != nil { 214 return err 215 } 216 } 217 return nil 218 } 219 220 func (d *dispatchRequest) getExpandedString(shlex *shell.Lex, str string) (string, error) { 221 substitutionArgs := []string{} 222 for key, value := range d.state.buildArgs.GetAllMeta() { 223 substitutionArgs = append(substitutionArgs, key+"="+value) 224 } 225 226 name, err := shlex.ProcessWord(str, substitutionArgs) 227 if err != nil { 228 return "", err 229 } 230 return name, nil 231 } 232 233 func (d *dispatchRequest) getImageOrStage(name string, platform *specs.Platform) (builder.Image, error) { 234 var localOnly bool 235 if im, ok := d.stages.getByName(name); ok { 236 name = im.Image 237 localOnly = true 238 } 239 240 if platform == nil { 241 platform = d.builder.platform 242 } 243 244 // Windows cannot support a container with no base image unless it is LCOW. 245 if name == api.NoBaseImageSpecifier { 246 p := platforms.DefaultSpec() 247 if platform != nil { 248 p = *platform 249 } 250 imageImage := &image.Image{} 251 imageImage.OS = p.OS 252 253 // old windows scratch handling 254 // TODO: scratch should not have an os. It should be nil image. 255 // Windows supports scratch. What is not supported is running containers 256 // from it. 257 if runtime.GOOS == "windows" { 258 if platform == nil || platform.OS == "linux" { 259 if !system.LCOWSupported() { 260 return nil, errors.New("Linux containers are not supported on this system") 261 } 262 imageImage.OS = "linux" 263 } else if platform.OS == "windows" { 264 return nil, errors.New("Windows does not support FROM scratch") 265 } else { 266 return nil, errors.Errorf("platform %s is not supported", platforms.Format(p)) 267 } 268 } 269 return builder.Image(imageImage), nil 270 } 271 imageMount, err := d.builder.imageSources.Get(name, localOnly, platform) 272 if err != nil { 273 return nil, err 274 } 275 return imageMount.Image(), nil 276 } 277 func (d *dispatchRequest) getFromImage(shlex *shell.Lex, basename string, platform *specs.Platform) (builder.Image, error) { 278 name, err := d.getExpandedString(shlex, basename) 279 if err != nil { 280 return nil, err 281 } 282 // Empty string is interpreted to FROM scratch by images.GetImageAndReleasableLayer, 283 // so validate expanded result is not empty. 284 if name == "" { 285 return nil, errors.Errorf("base name (%s) should not be blank", basename) 286 } 287 288 return d.getImageOrStage(name, platform) 289 } 290 291 func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { 292 d.state.runConfig.OnBuild = append(d.state.runConfig.OnBuild, c.Expression) 293 return d.builder.commit(d.state, "ONBUILD "+c.Expression) 294 } 295 296 // WORKDIR /tmp 297 // 298 // Set the working directory for future RUN/CMD/etc statements. 299 // 300 func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { 301 runConfig := d.state.runConfig 302 var err error 303 runConfig.WorkingDir, err = normalizeWorkdir(d.state.operatingSystem, runConfig.WorkingDir, c.Path) 304 if err != nil { 305 return err 306 } 307 308 // For performance reasons, we explicitly do a create/mkdir now 309 // This avoids having an unnecessary expensive mount/unmount calls 310 // (on Windows in particular) during each container create. 311 // Prior to 1.13, the mkdir was deferred and not executed at this step. 312 if d.builder.disableCommit { 313 // Don't call back into the daemon if we're going through docker commit --change "WORKDIR /foo". 314 // We've already updated the runConfig and that's enough. 315 return nil 316 } 317 318 comment := "WORKDIR " + runConfig.WorkingDir 319 runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.state.operatingSystem)) 320 321 containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) 322 if err != nil || containerID == "" { 323 return err 324 } 325 326 if err := d.builder.docker.ContainerCreateWorkdir(containerID); err != nil { 327 return err 328 } 329 330 return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd) 331 } 332 333 func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, os string) []string { 334 result := cmd.CmdLine 335 if cmd.PrependShell && result != nil { 336 result = append(getShell(runConfig, os), result...) 337 } 338 return result 339 } 340 341 // RUN some command yo 342 // 343 // run a command and commit the image. Args are automatically prepended with 344 // the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under 345 // Windows, in the event there is only one argument The difference in processing: 346 // 347 // RUN echo hi # sh -c echo hi (Linux and LCOW) 348 // RUN echo hi # cmd /S /C echo hi (Windows) 349 // RUN [ "echo", "hi" ] # echo hi 350 // 351 func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error { 352 if !system.IsOSSupported(d.state.operatingSystem) { 353 return system.ErrNotSupportedOperatingSystem 354 } 355 stateRunConfig := d.state.runConfig 356 cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.state.operatingSystem) 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 withEntrypointOverride(saveCmd, nil)) 367 if hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe); err != nil || hit { 368 return err 369 } 370 371 runConfig := copyRunConfig(stateRunConfig, 372 withCmd(cmdFromArgs), 373 withEnv(append(stateRunConfig.Env, buildArgs...)), 374 withEntrypointOverride(saveCmd, strslice.StrSlice{""}), 375 withoutHealthcheck()) 376 377 // set config as already being escaped, this prevents double escaping on windows 378 runConfig.ArgsEscaped = true 379 380 cID, err := d.builder.create(runConfig) 381 if err != nil { 382 return err 383 } 384 385 if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil { 386 if err, ok := err.(*statusCodeError); ok { 387 // TODO: change error type, because jsonmessage.JSONError assumes HTTP 388 msg := fmt.Sprintf( 389 "The command '%s' returned a non-zero code: %d", 390 strings.Join(runConfig.Cmd, " "), err.StatusCode()) 391 if err.Error() != "" { 392 msg = fmt.Sprintf("%s: %s", msg, err.Error()) 393 } 394 return &jsonmessage.JSONError{ 395 Message: msg, 396 Code: err.StatusCode(), 397 } 398 } 399 return err 400 } 401 402 return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe) 403 } 404 405 // Derive the command to use for probeCache() and to commit in this container. 406 // Note that we only do this if there are any build-time env vars. Also, we 407 // use the special argument "|#" at the start of the args array. This will 408 // avoid conflicts with any RUN command since commands can not 409 // start with | (vertical bar). The "#" (number of build envs) is there to 410 // help ensure proper cache matches. We don't want a RUN command 411 // that starts with "foo=abc" to be considered part of a build-time env var. 412 // 413 // remove any unreferenced built-in args from the environment variables. 414 // These args are transparent so resulting image should be the same regardless 415 // of the value. 416 func prependEnvOnCmd(buildArgs *BuildArgs, buildArgVars []string, cmd strslice.StrSlice) strslice.StrSlice { 417 var tmpBuildEnv []string 418 for _, env := range buildArgVars { 419 key := strings.SplitN(env, "=", 2)[0] 420 if buildArgs.IsReferencedOrNotBuiltin(key) { 421 tmpBuildEnv = append(tmpBuildEnv, env) 422 } 423 } 424 425 sort.Strings(tmpBuildEnv) 426 tmpEnv := append([]string{fmt.Sprintf("|%d", len(tmpBuildEnv))}, tmpBuildEnv...) 427 return strslice.StrSlice(append(tmpEnv, cmd...)) 428 } 429 430 // CMD foo 431 // 432 // Set the default command to run in the container (which may be empty). 433 // Argument handling is the same as RUN. 434 // 435 func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error { 436 runConfig := d.state.runConfig 437 cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem) 438 runConfig.Cmd = cmd 439 // set config as already being escaped, this prevents double escaping on windows 440 runConfig.ArgsEscaped = true 441 442 if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil { 443 return err 444 } 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 // 458 func dispatchHealthcheck(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(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 // 478 func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error { 479 runConfig := d.state.runConfig 480 cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem) 481 runConfig.Entrypoint = cmd 482 if !d.state.cmdSet { 483 runConfig.Cmd = nil 484 } 485 486 return d.builder.commit(d.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint)) 487 } 488 489 // EXPOSE 6667/tcp 7000/tcp 490 // 491 // Expose ports for links and port mappings. This all ends up in 492 // req.runConfig.ExposedPorts for runconfig. 493 // 494 func dispatchExpose(d dispatchRequest, c *instructions.ExposeCommand, envs []string) error { 495 // custom multi word expansion 496 // expose $FOO with FOO="80 443" is expanded as EXPOSE [80,443]. This is the only command supporting word to words expansion 497 // so the word processing has been de-generalized 498 ports := []string{} 499 for _, p := range c.Ports { 500 ps, err := d.shlex.ProcessWords(p, envs) 501 if err != nil { 502 return err 503 } 504 ports = append(ports, ps...) 505 } 506 c.Ports = ports 507 508 ps, _, err := nat.ParsePortSpecs(ports) 509 if err != nil { 510 return err 511 } 512 513 if d.state.runConfig.ExposedPorts == nil { 514 d.state.runConfig.ExposedPorts = make(nat.PortSet) 515 } 516 for p := range ps { 517 d.state.runConfig.ExposedPorts[p] = struct{}{} 518 } 519 520 return d.builder.commit(d.state, "EXPOSE "+strings.Join(c.Ports, " ")) 521 } 522 523 // USER foo 524 // 525 // Set the user to 'foo' for future commands and when running the 526 // ENTRYPOINT/CMD at container run time. 527 // 528 func dispatchUser(d dispatchRequest, c *instructions.UserCommand) error { 529 d.state.runConfig.User = c.User 530 return d.builder.commit(d.state, fmt.Sprintf("USER %v", c.User)) 531 } 532 533 // VOLUME /foo 534 // 535 // Expose the volume /foo for use. Will also accept the JSON array form. 536 // 537 func dispatchVolume(d dispatchRequest, c *instructions.VolumeCommand) error { 538 if d.state.runConfig.Volumes == nil { 539 d.state.runConfig.Volumes = map[string]struct{}{} 540 } 541 for _, v := range c.Volumes { 542 if v == "" { 543 return errors.New("VOLUME specified can not be an empty string") 544 } 545 d.state.runConfig.Volumes[v] = struct{}{} 546 } 547 return d.builder.commit(d.state, fmt.Sprintf("VOLUME %v", c.Volumes)) 548 } 549 550 // STOPSIGNAL signal 551 // 552 // Set the signal that will be used to kill the container. 553 func dispatchStopSignal(d dispatchRequest, c *instructions.StopSignalCommand) error { 554 555 _, err := signal.ParseSignal(c.Signal) 556 if err != nil { 557 return errdefs.InvalidParameter(err) 558 } 559 d.state.runConfig.StopSignal = c.Signal 560 return d.builder.commit(d.state, fmt.Sprintf("STOPSIGNAL %v", c.Signal)) 561 } 562 563 // ARG name[=value] 564 // 565 // Adds the variable foo to the trusted list of variables that can be passed 566 // to builder using the --build-arg flag for expansion/substitution or passing to 'run'. 567 // Dockerfile author may optionally set a default value of this variable. 568 func dispatchArg(d dispatchRequest, c *instructions.ArgCommand) error { 569 570 commitStr := "ARG " + c.Key 571 if c.Value != nil { 572 commitStr += "=" + *c.Value 573 } 574 575 d.state.buildArgs.AddArg(c.Key, c.Value) 576 return d.builder.commit(d.state, commitStr) 577 } 578 579 // SHELL powershell -command 580 // 581 // Set the non-default shell to use. 582 func dispatchShell(d dispatchRequest, c *instructions.ShellCommand) error { 583 d.state.runConfig.Shell = c.Shell 584 return d.builder.commit(d.state, fmt.Sprintf("SHELL %v", d.state.runConfig.Shell)) 585 }