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