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