github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/builder/dockerfile/evaluator.go (about)

     1  // Package dockerfile is the evaluation step in the Dockerfile parse/evaluate pipeline.
     2  //
     3  // It incorporates a dispatch table based on the parser.Node values (see the
     4  // parser package for more information) that are yielded from the parser itself.
     5  // Calling newBuilder with the BuildOpts struct can be used to customize the
     6  // experience for execution purposes only. Parsing is controlled in the parser
     7  // package, and this division of responsibility should be respected.
     8  //
     9  // Please see the jump table targets for the actual invocations, most of which
    10  // will call out to the functions in internals.go to deal with their tasks.
    11  //
    12  // ONBUILD is a special case, which is covered in the onbuild() func in
    13  // dispatchers.go.
    14  //
    15  // The evaluator uses the concept of "steps", which are usually each processable
    16  // line in the Dockerfile. Each step is numbered and certain actions are taken
    17  // before and after each step, such as creating an image ID and removing temporary
    18  // containers and images. Note that ONBUILD creates a kinda-sorta "sub run" which
    19  // includes its own set of steps (usually only one of them).
    20  package dockerfile
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/docker/docker/api/types/container"
    28  	"github.com/docker/docker/builder"
    29  	"github.com/docker/docker/builder/dockerfile/command"
    30  	"github.com/docker/docker/builder/dockerfile/parser"
    31  	"github.com/docker/docker/pkg/system"
    32  	"github.com/docker/docker/runconfig/opts"
    33  	"github.com/pkg/errors"
    34  )
    35  
    36  // Environment variable interpolation will happen on these statements only.
    37  var replaceEnvAllowed = map[string]bool{
    38  	command.Env:        true,
    39  	command.Label:      true,
    40  	command.Add:        true,
    41  	command.Copy:       true,
    42  	command.Workdir:    true,
    43  	command.Expose:     true,
    44  	command.Volume:     true,
    45  	command.User:       true,
    46  	command.StopSignal: true,
    47  	command.Arg:        true,
    48  }
    49  
    50  // Certain commands are allowed to have their args split into more
    51  // words after env var replacements. Meaning:
    52  //   ENV foo="123 456"
    53  //   EXPOSE $foo
    54  // should result in the same thing as:
    55  //   EXPOSE 123 456
    56  // and not treat "123 456" as a single word.
    57  // Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing.
    58  // Quotes will cause it to still be treated as single word.
    59  var allowWordExpansion = map[string]bool{
    60  	command.Expose: true,
    61  }
    62  
    63  type dispatchRequest struct {
    64  	builder    *Builder // TODO: replace this with a smaller interface
    65  	args       []string
    66  	attributes map[string]bool
    67  	flags      *BFlags
    68  	original   string
    69  	shlex      *ShellLex
    70  	state      *dispatchState
    71  	source     builder.Source
    72  }
    73  
    74  func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest {
    75  	return dispatchRequest{
    76  		builder:    builder,
    77  		args:       args,
    78  		attributes: options.node.Attributes,
    79  		original:   options.node.Original,
    80  		flags:      NewBFlagsWithArgs(options.node.Flags),
    81  		shlex:      options.shlex,
    82  		state:      options.state,
    83  		source:     options.source,
    84  	}
    85  }
    86  
    87  type dispatcher func(dispatchRequest) error
    88  
    89  var evaluateTable map[string]dispatcher
    90  
    91  func init() {
    92  	evaluateTable = map[string]dispatcher{
    93  		command.Add:         add,
    94  		command.Arg:         arg,
    95  		command.Cmd:         cmd,
    96  		command.Copy:        dispatchCopy, // copy() is a go builtin
    97  		command.Entrypoint:  entrypoint,
    98  		command.Env:         env,
    99  		command.Expose:      expose,
   100  		command.From:        from,
   101  		command.Healthcheck: healthcheck,
   102  		command.Label:       label,
   103  		command.Maintainer:  maintainer,
   104  		command.Onbuild:     onbuild,
   105  		command.Run:         run,
   106  		command.Shell:       shell,
   107  		command.StopSignal:  stopSignal,
   108  		command.User:        user,
   109  		command.Volume:      volume,
   110  		command.Workdir:     workdir,
   111  	}
   112  }
   113  
   114  func formatStep(stepN int, stepTotal int) string {
   115  	return fmt.Sprintf("%d/%d", stepN+1, stepTotal)
   116  }
   117  
   118  // This method is the entrypoint to all statement handling routines.
   119  //
   120  // Almost all nodes will have this structure:
   121  // Child[Node, Node, Node] where Child is from parser.Node.Children and each
   122  // node comes from parser.Node.Next. This forms a "line" with a statement and
   123  // arguments and we process them in this normalized form by hitting
   124  // evaluateTable with the leaf nodes of the command and the Builder object.
   125  //
   126  // ONBUILD is a special case; in this case the parser will emit:
   127  // Child[Node, Child[Node, Node...]] where the first node is the literal
   128  // "onbuild" and the child entrypoint is the command of the ONBUILD statement,
   129  // such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
   130  // deal with that, at least until it becomes more of a general concern with new
   131  // features.
   132  func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
   133  	node := options.node
   134  	cmd := node.Value
   135  	upperCasedCmd := strings.ToUpper(cmd)
   136  
   137  	// To ensure the user is given a decent error message if the platform
   138  	// on which the daemon is running does not support a builder command.
   139  	if err := platformSupports(strings.ToLower(cmd)); err != nil {
   140  		buildsFailed.WithValues(metricsCommandNotSupportedError).Inc()
   141  		return nil, err
   142  	}
   143  
   144  	msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
   145  		options.stepMsg, upperCasedCmd, formatFlags(node.Flags)))
   146  
   147  	args := []string{}
   148  	ast := node
   149  	if cmd == command.Onbuild {
   150  		var err error
   151  		ast, args, err = handleOnBuildNode(node, msg)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  	}
   156  
   157  	runConfigEnv := options.state.runConfig.Env
   158  	envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...)
   159  	processFunc := createProcessWordFunc(options.shlex, cmd, envs)
   160  	words, err := getDispatchArgsFromNode(ast, processFunc, msg)
   161  	if err != nil {
   162  		buildsFailed.WithValues(metricsErrorProcessingCommandsError).Inc()
   163  		return nil, err
   164  	}
   165  	args = append(args, words...)
   166  
   167  	fmt.Fprintln(b.Stdout, msg.String())
   168  
   169  	f, ok := evaluateTable[cmd]
   170  	if !ok {
   171  		buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
   172  		return nil, fmt.Errorf("unknown instruction: %s", upperCasedCmd)
   173  	}
   174  	options.state.updateRunConfig()
   175  	err = f(newDispatchRequestFromOptions(options, b, args))
   176  	return options.state, err
   177  }
   178  
   179  type dispatchOptions struct {
   180  	state   *dispatchState
   181  	stepMsg string
   182  	node    *parser.Node
   183  	shlex   *ShellLex
   184  	source  builder.Source
   185  }
   186  
   187  // dispatchState is a data object which is modified by dispatchers
   188  type dispatchState struct {
   189  	runConfig  *container.Config
   190  	maintainer string
   191  	cmdSet     bool
   192  	imageID    string
   193  	baseImage  builder.Image
   194  	stageName  string
   195  }
   196  
   197  func newDispatchState() *dispatchState {
   198  	return &dispatchState{runConfig: &container.Config{}}
   199  }
   200  
   201  func (s *dispatchState) updateRunConfig() {
   202  	s.runConfig.Image = s.imageID
   203  }
   204  
   205  // hasFromImage returns true if the builder has processed a `FROM <image>` line
   206  func (s *dispatchState) hasFromImage() bool {
   207  	return s.imageID != "" || (s.baseImage != nil && s.baseImage.ImageID() == "")
   208  }
   209  
   210  func (s *dispatchState) isCurrentStage(target string) bool {
   211  	if target == "" {
   212  		return false
   213  	}
   214  	return strings.EqualFold(s.stageName, target)
   215  }
   216  
   217  func (s *dispatchState) beginStage(stageName string, image builder.Image) {
   218  	s.stageName = stageName
   219  	s.imageID = image.ImageID()
   220  
   221  	if image.RunConfig() != nil {
   222  		s.runConfig = image.RunConfig()
   223  	} else {
   224  		s.runConfig = &container.Config{}
   225  	}
   226  	s.baseImage = image
   227  	s.setDefaultPath()
   228  }
   229  
   230  // Add the default PATH to runConfig.ENV if one exists for the platform and there
   231  // is no PATH set. Note that windows won't have one as it's set by HCS
   232  func (s *dispatchState) setDefaultPath() {
   233  	if system.DefaultPathEnv == "" {
   234  		return
   235  	}
   236  	envMap := opts.ConvertKVStringsToMap(s.runConfig.Env)
   237  	if _, ok := envMap["PATH"]; !ok {
   238  		s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv)
   239  	}
   240  }
   241  
   242  func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
   243  	if ast.Next == nil {
   244  		return nil, nil, errors.New("ONBUILD requires at least one argument")
   245  	}
   246  	ast = ast.Next.Children[0]
   247  	msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
   248  	return ast, []string{ast.Value}, nil
   249  }
   250  
   251  func formatFlags(flags []string) string {
   252  	if len(flags) > 0 {
   253  		return " " + strings.Join(flags, " ")
   254  	}
   255  	return ""
   256  }
   257  
   258  func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) {
   259  	args := []string{}
   260  	for i := 0; ast.Next != nil; i++ {
   261  		ast = ast.Next
   262  		words, err := processFunc(ast.Value)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  		args = append(args, words...)
   267  		msg.WriteString(" " + ast.Value)
   268  	}
   269  	return args, nil
   270  }
   271  
   272  type processWordFunc func(string) ([]string, error)
   273  
   274  func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
   275  	switch {
   276  	case !replaceEnvAllowed[cmd]:
   277  		return func(word string) ([]string, error) {
   278  			return []string{word}, nil
   279  		}
   280  	case allowWordExpansion[cmd]:
   281  		return func(word string) ([]string, error) {
   282  			return shlex.ProcessWords(word, envs)
   283  		}
   284  	default:
   285  		return func(word string) ([]string, error) {
   286  			word, err := shlex.ProcessWord(word, envs)
   287  			return []string{word}, err
   288  		}
   289  	}
   290  }
   291  
   292  // checkDispatch does a simple check for syntax errors of the Dockerfile.
   293  // Because some of the instructions can only be validated through runtime,
   294  // arg, env, etc., this syntax check will not be complete and could not replace
   295  // the runtime check. Instead, this function is only a helper that allows
   296  // user to find out the obvious error in Dockerfile earlier on.
   297  func checkDispatch(ast *parser.Node) error {
   298  	cmd := ast.Value
   299  	upperCasedCmd := strings.ToUpper(cmd)
   300  
   301  	// To ensure the user is given a decent error message if the platform
   302  	// on which the daemon is running does not support a builder command.
   303  	if err := platformSupports(strings.ToLower(cmd)); err != nil {
   304  		return err
   305  	}
   306  
   307  	// The instruction itself is ONBUILD, we will make sure it follows with at
   308  	// least one argument
   309  	if upperCasedCmd == "ONBUILD" {
   310  		if ast.Next == nil {
   311  			buildsFailed.WithValues(metricsMissingOnbuildArgumentsError).Inc()
   312  			return errors.New("ONBUILD requires at least one argument")
   313  		}
   314  	}
   315  
   316  	if _, ok := evaluateTable[cmd]; ok {
   317  		return nil
   318  	}
   319  	buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
   320  	return errors.Errorf("unknown instruction: %s", upperCasedCmd)
   321  }