github.com/lazyboychen7/engine@v17.12.1-ce-rc2+incompatible/builder/dockerfile/dispatchers.go (about) 1 package 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/docker/docker/api" 18 "github.com/docker/docker/api/types/container" 19 "github.com/docker/docker/api/types/strslice" 20 "github.com/docker/docker/builder" 21 "github.com/docker/docker/builder/dockerfile/instructions" 22 "github.com/docker/docker/builder/dockerfile/parser" 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/pkg/errors" 29 "github.com/sirupsen/logrus" 30 ) 31 32 // ENV foo bar 33 // 34 // Sets the environment variable foo to bar, also makes interpolation 35 // in the dockerfile available from the next statement on via ${foo}. 36 // 37 func dispatchEnv(d dispatchRequest, c *instructions.EnvCommand) error { 38 runConfig := d.state.runConfig 39 commitMessage := bytes.NewBufferString("ENV") 40 for _, e := range c.Env { 41 name := e.Key 42 newVar := e.String() 43 44 commitMessage.WriteString(" " + newVar) 45 gotOne := false 46 for i, envVar := range runConfig.Env { 47 envParts := strings.SplitN(envVar, "=", 2) 48 compareFrom := envParts[0] 49 if 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(d.state, commitMessage.String()) 60 } 61 62 // MAINTAINER some text <maybe@an.email.address> 63 // 64 // Sets the maintainer metadata. 65 func dispatchMaintainer(d dispatchRequest, c *instructions.MaintainerCommand) error { 66 67 d.state.maintainer = c.Maintainer 68 return d.builder.commit(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 // 75 func dispatchLabel(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(d.state, commitStr) 85 } 86 87 // ADD foo /path 88 // 89 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling 90 // exist here. If you do not wish to have this automatic handling, use COPY. 91 // 92 func dispatchAdd(d dispatchRequest, c *instructions.AddCommand) error { 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(d.state, copyInstruction) 105 } 106 107 // COPY foo /path 108 // 109 // Same as 'ADD' but without the tar and remote url handling. 110 // 111 func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error { 112 var im *imageMount 113 var err error 114 if c.From != "" { 115 im, err = d.getImageMount(c.From) 116 if err != nil { 117 return errors.Wrapf(err, "invalid from flag value %s", c.From) 118 } 119 } 120 copier := copierFromDispatchRequest(d, errOnSourceDownload, im) 121 defer copier.Cleanup() 122 copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "COPY") 123 if err != nil { 124 return err 125 } 126 copyInstruction.chownStr = c.Chown 127 128 return d.builder.performCopy(d.state, copyInstruction) 129 } 130 131 func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error) { 132 if imageRefOrID == "" { 133 // TODO: this could return the source in the default case as well? 134 return nil, nil 135 } 136 137 var localOnly bool 138 stage, err := d.stages.get(imageRefOrID) 139 if err != nil { 140 return nil, err 141 } 142 if stage != nil { 143 imageRefOrID = stage.Image 144 localOnly = true 145 } 146 return d.builder.imageSources.Get(imageRefOrID, localOnly) 147 } 148 149 // FROM imagename[:tag | @digest] [AS build-stage-name] 150 // 151 func initializeStage(d dispatchRequest, cmd *instructions.Stage) error { 152 d.builder.imageProber.Reset() 153 image, err := d.getFromImage(d.shlex, cmd.BaseName) 154 if err != nil { 155 return err 156 } 157 state := d.state 158 state.beginStage(cmd.Name, image) 159 if len(state.runConfig.OnBuild) > 0 { 160 triggers := state.runConfig.OnBuild 161 state.runConfig.OnBuild = nil 162 return dispatchTriggeredOnBuild(d, triggers) 163 } 164 return nil 165 } 166 167 func dispatchTriggeredOnBuild(d dispatchRequest, triggers []string) error { 168 fmt.Fprintf(d.builder.Stdout, "# Executing %d build trigger", len(triggers)) 169 if len(triggers) > 1 { 170 fmt.Fprint(d.builder.Stdout, "s") 171 } 172 fmt.Fprintln(d.builder.Stdout) 173 for _, trigger := range triggers { 174 d.state.updateRunConfig() 175 ast, err := parser.Parse(strings.NewReader(trigger)) 176 if err != nil { 177 return err 178 } 179 if len(ast.AST.Children) != 1 { 180 return errors.New("onbuild trigger should be a single expression") 181 } 182 cmd, err := instructions.ParseCommand(ast.AST.Children[0]) 183 if err != nil { 184 if instructions.IsUnknownInstruction(err) { 185 buildsFailed.WithValues(metricsUnknownInstructionError).Inc() 186 } 187 return err 188 } 189 err = dispatch(d, cmd) 190 if err != nil { 191 return err 192 } 193 } 194 return nil 195 } 196 197 func (d *dispatchRequest) getExpandedImageName(shlex *ShellLex, name string) (string, error) { 198 substitutionArgs := []string{} 199 for key, value := range d.state.buildArgs.GetAllMeta() { 200 substitutionArgs = append(substitutionArgs, key+"="+value) 201 } 202 203 name, err := shlex.ProcessWord(name, substitutionArgs) 204 if err != nil { 205 return "", err 206 } 207 return name, nil 208 } 209 func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) { 210 var localOnly bool 211 if im, ok := d.stages.getByName(name); ok { 212 name = im.Image 213 localOnly = true 214 } 215 216 // Windows cannot support a container with no base image unless it is LCOW. 217 if name == api.NoBaseImageSpecifier { 218 imageImage := &image.Image{} 219 imageImage.OS = runtime.GOOS 220 if runtime.GOOS == "windows" { 221 optionsOS := system.ParsePlatform(d.builder.options.Platform).OS 222 switch optionsOS { 223 case "windows", "": 224 return nil, errors.New("Windows does not support FROM scratch") 225 case "linux": 226 if !system.LCOWSupported() { 227 return nil, errors.New("Linux containers are not supported on this system") 228 } 229 imageImage.OS = "linux" 230 default: 231 return nil, errors.Errorf("operating system %q is not supported", optionsOS) 232 } 233 } 234 return builder.Image(imageImage), nil 235 } 236 imageMount, err := d.builder.imageSources.Get(name, localOnly) 237 if err != nil { 238 return nil, err 239 } 240 return imageMount.Image(), nil 241 } 242 func (d *dispatchRequest) getFromImage(shlex *ShellLex, name string) (builder.Image, error) { 243 name, err := d.getExpandedImageName(shlex, name) 244 if err != nil { 245 return nil, err 246 } 247 return d.getImageOrStage(name) 248 } 249 250 func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { 251 252 d.state.runConfig.OnBuild = append(d.state.runConfig.OnBuild, c.Expression) 253 return d.builder.commit(d.state, "ONBUILD "+c.Expression) 254 } 255 256 // WORKDIR /tmp 257 // 258 // Set the working directory for future RUN/CMD/etc statements. 259 // 260 func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { 261 runConfig := d.state.runConfig 262 var err error 263 optionsOS := system.ParsePlatform(d.builder.options.Platform).OS 264 runConfig.WorkingDir, err = normalizeWorkdir(optionsOS, runConfig.WorkingDir, c.Path) 265 if err != nil { 266 return err 267 } 268 269 // For performance reasons, we explicitly do a create/mkdir now 270 // This avoids having an unnecessary expensive mount/unmount calls 271 // (on Windows in particular) during each container create. 272 // Prior to 1.13, the mkdir was deferred and not executed at this step. 273 if d.builder.disableCommit { 274 // Don't call back into the daemon if we're going through docker commit --change "WORKDIR /foo". 275 // We've already updated the runConfig and that's enough. 276 return nil 277 } 278 279 comment := "WORKDIR " + runConfig.WorkingDir 280 runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, optionsOS)) 281 containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) 282 if err != nil || containerID == "" { 283 return err 284 } 285 if err := d.builder.docker.ContainerCreateWorkdir(containerID); err != nil { 286 return err 287 } 288 289 return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd) 290 } 291 292 func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, platform string) []string { 293 result := cmd.CmdLine 294 if cmd.PrependShell && result != nil { 295 result = append(getShell(runConfig, platform), result...) 296 } 297 return result 298 } 299 300 // RUN some command yo 301 // 302 // run a command and commit the image. Args are automatically prepended with 303 // the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under 304 // Windows, in the event there is only one argument The difference in processing: 305 // 306 // RUN echo hi # sh -c echo hi (Linux and LCOW) 307 // RUN echo hi # cmd /S /C echo hi (Windows) 308 // RUN [ "echo", "hi" ] # echo hi 309 // 310 func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error { 311 312 stateRunConfig := d.state.runConfig 313 optionsOS := system.ParsePlatform(d.builder.options.Platform).OS 314 cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, optionsOS) 315 buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env) 316 317 saveCmd := cmdFromArgs 318 if len(buildArgs) > 0 { 319 saveCmd = prependEnvOnCmd(d.state.buildArgs, buildArgs, cmdFromArgs) 320 } 321 322 runConfigForCacheProbe := copyRunConfig(stateRunConfig, 323 withCmd(saveCmd), 324 withEntrypointOverride(saveCmd, nil)) 325 hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe) 326 if err != nil || hit { 327 return err 328 } 329 330 runConfig := copyRunConfig(stateRunConfig, 331 withCmd(cmdFromArgs), 332 withEnv(append(stateRunConfig.Env, buildArgs...)), 333 withEntrypointOverride(saveCmd, strslice.StrSlice{""})) 334 335 // set config as already being escaped, this prevents double escaping on windows 336 runConfig.ArgsEscaped = true 337 338 logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd) 339 cID, err := d.builder.create(runConfig) 340 if err != nil { 341 return err 342 } 343 if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil { 344 if err, ok := err.(*statusCodeError); ok { 345 // TODO: change error type, because jsonmessage.JSONError assumes HTTP 346 return &jsonmessage.JSONError{ 347 Message: fmt.Sprintf( 348 "The command '%s' returned a non-zero code: %d", 349 strings.Join(runConfig.Cmd, " "), err.StatusCode()), 350 Code: err.StatusCode(), 351 } 352 } 353 return err 354 } 355 356 return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe) 357 } 358 359 // Derive the command to use for probeCache() and to commit in this container. 360 // Note that we only do this if there are any build-time env vars. Also, we 361 // use the special argument "|#" at the start of the args array. This will 362 // avoid conflicts with any RUN command since commands can not 363 // start with | (vertical bar). The "#" (number of build envs) is there to 364 // help ensure proper cache matches. We don't want a RUN command 365 // that starts with "foo=abc" to be considered part of a build-time env var. 366 // 367 // remove any unreferenced built-in args from the environment variables. 368 // These args are transparent so resulting image should be the same regardless 369 // of the value. 370 func prependEnvOnCmd(buildArgs *buildArgs, buildArgVars []string, cmd strslice.StrSlice) strslice.StrSlice { 371 var tmpBuildEnv []string 372 for _, env := range buildArgVars { 373 key := strings.SplitN(env, "=", 2)[0] 374 if buildArgs.IsReferencedOrNotBuiltin(key) { 375 tmpBuildEnv = append(tmpBuildEnv, env) 376 } 377 } 378 379 sort.Strings(tmpBuildEnv) 380 tmpEnv := append([]string{fmt.Sprintf("|%d", len(tmpBuildEnv))}, tmpBuildEnv...) 381 return strslice.StrSlice(append(tmpEnv, cmd...)) 382 } 383 384 // CMD foo 385 // 386 // Set the default command to run in the container (which may be empty). 387 // Argument handling is the same as RUN. 388 // 389 func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error { 390 runConfig := d.state.runConfig 391 optionsOS := system.ParsePlatform(d.builder.options.Platform).OS 392 cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, optionsOS) 393 runConfig.Cmd = cmd 394 // set config as already being escaped, this prevents double escaping on windows 395 runConfig.ArgsEscaped = true 396 397 if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil { 398 return err 399 } 400 401 if len(c.ShellDependantCmdLine.CmdLine) != 0 { 402 d.state.cmdSet = true 403 } 404 405 return nil 406 } 407 408 // HEALTHCHECK foo 409 // 410 // Set the default healthcheck command to run in the container (which may be empty). 411 // Argument handling is the same as RUN. 412 // 413 func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) error { 414 runConfig := d.state.runConfig 415 if runConfig.Healthcheck != nil { 416 oldCmd := runConfig.Healthcheck.Test 417 if len(oldCmd) > 0 && oldCmd[0] != "NONE" { 418 fmt.Fprintf(d.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) 419 } 420 } 421 runConfig.Healthcheck = c.Health 422 return d.builder.commit(d.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck)) 423 } 424 425 // ENTRYPOINT /usr/sbin/nginx 426 // 427 // Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments 428 // to /usr/sbin/nginx. Uses the default shell if not in JSON format. 429 // 430 // Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint 431 // is initialized at newBuilder time instead of through argument parsing. 432 // 433 func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error { 434 runConfig := d.state.runConfig 435 optionsOS := system.ParsePlatform(d.builder.options.Platform).OS 436 cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, optionsOS) 437 runConfig.Entrypoint = cmd 438 if !d.state.cmdSet { 439 runConfig.Cmd = nil 440 } 441 442 return d.builder.commit(d.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint)) 443 } 444 445 // EXPOSE 6667/tcp 7000/tcp 446 // 447 // Expose ports for links and port mappings. This all ends up in 448 // req.runConfig.ExposedPorts for runconfig. 449 // 450 func dispatchExpose(d dispatchRequest, c *instructions.ExposeCommand, envs []string) error { 451 // custom multi word expansion 452 // expose $FOO with FOO="80 443" is expanded as EXPOSE [80,443]. This is the only command supporting word to words expansion 453 // so the word processing has been de-generalized 454 ports := []string{} 455 for _, p := range c.Ports { 456 ps, err := d.shlex.ProcessWords(p, envs) 457 if err != nil { 458 return err 459 } 460 ports = append(ports, ps...) 461 } 462 c.Ports = ports 463 464 ps, _, err := nat.ParsePortSpecs(ports) 465 if err != nil { 466 return err 467 } 468 469 if d.state.runConfig.ExposedPorts == nil { 470 d.state.runConfig.ExposedPorts = make(nat.PortSet) 471 } 472 for p := range ps { 473 d.state.runConfig.ExposedPorts[p] = struct{}{} 474 } 475 476 return d.builder.commit(d.state, "EXPOSE "+strings.Join(c.Ports, " ")) 477 } 478 479 // USER foo 480 // 481 // Set the user to 'foo' for future commands and when running the 482 // ENTRYPOINT/CMD at container run time. 483 // 484 func dispatchUser(d dispatchRequest, c *instructions.UserCommand) error { 485 d.state.runConfig.User = c.User 486 return d.builder.commit(d.state, fmt.Sprintf("USER %v", c.User)) 487 } 488 489 // VOLUME /foo 490 // 491 // Expose the volume /foo for use. Will also accept the JSON array form. 492 // 493 func dispatchVolume(d dispatchRequest, c *instructions.VolumeCommand) error { 494 if d.state.runConfig.Volumes == nil { 495 d.state.runConfig.Volumes = map[string]struct{}{} 496 } 497 for _, v := range c.Volumes { 498 if v == "" { 499 return errors.New("VOLUME specified can not be an empty string") 500 } 501 d.state.runConfig.Volumes[v] = struct{}{} 502 } 503 return d.builder.commit(d.state, fmt.Sprintf("VOLUME %v", c.Volumes)) 504 } 505 506 // STOPSIGNAL signal 507 // 508 // Set the signal that will be used to kill the container. 509 func dispatchStopSignal(d dispatchRequest, c *instructions.StopSignalCommand) error { 510 511 _, err := signal.ParseSignal(c.Signal) 512 if err != nil { 513 return validationError{err} 514 } 515 d.state.runConfig.StopSignal = c.Signal 516 return d.builder.commit(d.state, fmt.Sprintf("STOPSIGNAL %v", c.Signal)) 517 } 518 519 // ARG name[=value] 520 // 521 // Adds the variable foo to the trusted list of variables that can be passed 522 // to builder using the --build-arg flag for expansion/substitution or passing to 'run'. 523 // Dockerfile author may optionally set a default value of this variable. 524 func dispatchArg(d dispatchRequest, c *instructions.ArgCommand) error { 525 526 commitStr := "ARG " + c.Key 527 if c.Value != nil { 528 commitStr += "=" + *c.Value 529 } 530 531 d.state.buildArgs.AddArg(c.Key, c.Value) 532 return d.builder.commit(d.state, commitStr) 533 } 534 535 // SHELL powershell -command 536 // 537 // Set the non-default shell to use. 538 func dispatchShell(d dispatchRequest, c *instructions.ShellCommand) error { 539 d.state.runConfig.Shell = c.Shell 540 return d.builder.commit(d.state, fmt.Sprintf("SHELL %v", d.state.runConfig.Shell)) 541 }