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 }