github.com/sijibomii/docker@v0.0.0-20231230191044-5cf6ca554647/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 "fmt" 24 "strings" 25 26 "github.com/docker/docker/builder/dockerfile/command" 27 "github.com/docker/docker/builder/dockerfile/parser" 28 ) 29 30 // Environment variable interpolation will happen on these statements only. 31 var replaceEnvAllowed = map[string]bool{ 32 command.Env: true, 33 command.Label: true, 34 command.Add: true, 35 command.Copy: true, 36 command.Workdir: true, 37 command.Expose: true, 38 command.Volume: true, 39 command.User: true, 40 command.StopSignal: true, 41 command.Arg: true, 42 } 43 44 // Certain commands are allowed to have their args split into more 45 // words after env var replacements. Meaning: 46 // ENV foo="123 456" 47 // EXPOSE $foo 48 // should result in the same thing as: 49 // EXPOSE 123 456 50 // and not treat "123 456" as a single word. 51 // Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing. 52 // Quotes will cause it to still be treated as single word. 53 var allowWordExpansion = map[string]bool{ 54 command.Expose: true, 55 } 56 57 var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error 58 59 func init() { 60 evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{ 61 command.Env: env, 62 command.Label: label, 63 command.Maintainer: maintainer, 64 command.Add: add, 65 command.Copy: dispatchCopy, // copy() is a go builtin 66 command.From: from, 67 command.Onbuild: onbuild, 68 command.Workdir: workdir, 69 command.Run: run, 70 command.Cmd: cmd, 71 command.Entrypoint: entrypoint, 72 command.Expose: expose, 73 command.Volume: volume, 74 command.User: user, 75 command.StopSignal: stopSignal, 76 command.Arg: arg, 77 } 78 } 79 80 // This method is the entrypoint to all statement handling routines. 81 // 82 // Almost all nodes will have this structure: 83 // Child[Node, Node, Node] where Child is from parser.Node.Children and each 84 // node comes from parser.Node.Next. This forms a "line" with a statement and 85 // arguments and we process them in this normalized form by hitting 86 // evaluateTable with the leaf nodes of the command and the Builder object. 87 // 88 // ONBUILD is a special case; in this case the parser will emit: 89 // Child[Node, Child[Node, Node...]] where the first node is the literal 90 // "onbuild" and the child entrypoint is the command of the ONBUILD statement, 91 // such as `RUN` in ONBUILD RUN foo. There is special case logic in here to 92 // deal with that, at least until it becomes more of a general concern with new 93 // features. 94 func (b *Builder) dispatch(stepN int, ast *parser.Node) error { 95 cmd := ast.Value 96 upperCasedCmd := strings.ToUpper(cmd) 97 98 // To ensure the user is given a decent error message if the platform 99 // on which the daemon is running does not support a builder command. 100 if err := platformSupports(strings.ToLower(cmd)); err != nil { 101 return err 102 } 103 104 attrs := ast.Attributes 105 original := ast.Original 106 flags := ast.Flags 107 strList := []string{} 108 msg := fmt.Sprintf("Step %d : %s", stepN+1, upperCasedCmd) 109 110 if len(ast.Flags) > 0 { 111 msg += " " + strings.Join(ast.Flags, " ") 112 } 113 114 if cmd == "onbuild" { 115 if ast.Next == nil { 116 return fmt.Errorf("ONBUILD requires at least one argument") 117 } 118 ast = ast.Next.Children[0] 119 strList = append(strList, ast.Value) 120 msg += " " + ast.Value 121 122 if len(ast.Flags) > 0 { 123 msg += " " + strings.Join(ast.Flags, " ") 124 } 125 126 } 127 128 // count the number of nodes that we are going to traverse first 129 // so we can pre-create the argument and message array. This speeds up the 130 // allocation of those list a lot when they have a lot of arguments 131 cursor := ast 132 var n int 133 for cursor.Next != nil { 134 cursor = cursor.Next 135 n++ 136 } 137 msgList := make([]string, n) 138 139 var i int 140 // Append the build-time args to config-environment. 141 // This allows builder config to override the variables, making the behavior similar to 142 // a shell script i.e. `ENV foo bar` overrides value of `foo` passed in build 143 // context. But `ENV foo $foo` will use the value from build context if one 144 // isn't already been defined by a previous ENV primitive. 145 // Note, we get this behavior because we know that ProcessWord() will 146 // stop on the first occurrence of a variable name and not notice 147 // a subsequent one. So, putting the buildArgs list after the Config.Env 148 // list, in 'envs', is safe. 149 envs := b.runConfig.Env 150 for key, val := range b.options.BuildArgs { 151 if !b.isBuildArgAllowed(key) { 152 // skip build-args that are not in allowed list, meaning they have 153 // not been defined by an "ARG" Dockerfile command yet. 154 // This is an error condition but only if there is no "ARG" in the entire 155 // Dockerfile, so we'll generate any necessary errors after we parsed 156 // the entire file (see 'leftoverArgs' processing in evaluator.go ) 157 continue 158 } 159 envs = append(envs, fmt.Sprintf("%s=%s", key, val)) 160 } 161 for ast.Next != nil { 162 ast = ast.Next 163 var str string 164 str = ast.Value 165 if replaceEnvAllowed[cmd] { 166 var err error 167 var words []string 168 169 if allowWordExpansion[cmd] { 170 words, err = ProcessWords(str, envs) 171 if err != nil { 172 return err 173 } 174 strList = append(strList, words...) 175 } else { 176 str, err = ProcessWord(str, envs) 177 if err != nil { 178 return err 179 } 180 strList = append(strList, str) 181 } 182 } else { 183 strList = append(strList, str) 184 } 185 msgList[i] = ast.Value 186 i++ 187 } 188 189 msg += " " + strings.Join(msgList, " ") 190 fmt.Fprintln(b.Stdout, msg) 191 192 // XXX yes, we skip any cmds that are not valid; the parser should have 193 // picked these out already. 194 if f, ok := evaluateTable[cmd]; ok { 195 b.flags = NewBFlags() 196 b.flags.Args = flags 197 return f(b, strList, attrs, original) 198 } 199 200 return fmt.Errorf("Unknown instruction: %s", upperCasedCmd) 201 }