-
Notifications
You must be signed in to change notification settings - Fork 18.6k
/
evaluator.go
244 lines (219 loc) · 8.26 KB
/
evaluator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Package dockerfile is the evaluation step in the Dockerfile parse/evaluate pipeline.
//
// It incorporates a dispatch table based on the parser.Node values (see the
// parser package for more information) that are yielded from the parser itself.
// Calling NewBuilder with the BuildOpts struct can be used to customize the
// experience for execution purposes only. Parsing is controlled in the parser
// package, and this division of responsibility should be respected.
//
// Please see the jump table targets for the actual invocations, most of which
// will call out to the functions in internals.go to deal with their tasks.
//
// ONBUILD is a special case, which is covered in the onbuild() func in
// dispatchers.go.
//
// The evaluator uses the concept of "steps", which are usually each processable
// line in the Dockerfile. Each step is numbered and certain actions are taken
// before and after each step, such as creating an image ID and removing temporary
// containers and images. Note that ONBUILD creates a kinda-sorta "sub run" which
// includes its own set of steps (usually only one of them).
package dockerfile
import (
"fmt"
"strings"
"github.com/docker/docker/builder/dockerfile/command"
"github.com/docker/docker/builder/dockerfile/parser"
)
// Environment variable interpolation will happen on these statements only.
var replaceEnvAllowed = map[string]bool{
command.Env: true,
command.Label: true,
command.Add: true,
command.Copy: true,
command.Workdir: true,
command.Expose: true,
command.Volume: true,
command.User: true,
command.StopSignal: true,
command.Arg: true,
}
// Certain commands are allowed to have their args split into more
// words after env var replacements. Meaning:
// ENV foo="123 456"
// EXPOSE $foo
// should result in the same thing as:
// EXPOSE 123 456
// and not treat "123 456" as a single word.
// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing.
// Quotes will cause it to still be treated as single word.
var allowWordExpansion = map[string]bool{
command.Expose: true,
}
var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error
func init() {
evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
command.Add: add,
command.Arg: arg,
command.Cmd: cmd,
command.Copy: dispatchCopy, // copy() is a go builtin
command.Entrypoint: entrypoint,
command.Env: env,
command.Expose: expose,
command.From: from,
command.Healthcheck: healthcheck,
command.Label: label,
command.Maintainer: maintainer,
command.Onbuild: onbuild,
command.Run: run,
command.Shell: shell,
command.StopSignal: stopSignal,
command.User: user,
command.Volume: volume,
command.Workdir: workdir,
}
}
// This method is the entrypoint to all statement handling routines.
//
// Almost all nodes will have this structure:
// Child[Node, Node, Node] where Child is from parser.Node.Children and each
// node comes from parser.Node.Next. This forms a "line" with a statement and
// arguments and we process them in this normalized form by hitting
// evaluateTable with the leaf nodes of the command and the Builder object.
//
// ONBUILD is a special case; in this case the parser will emit:
// Child[Node, Child[Node, Node...]] where the first node is the literal
// "onbuild" and the child entrypoint is the command of the ONBUILD statement,
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
// deal with that, at least until it becomes more of a general concern with new
// features.
func (b *Builder) dispatch(stepN int, stepTotal int, ast *parser.Node) error {
cmd := ast.Value
upperCasedCmd := strings.ToUpper(cmd)
// To ensure the user is given a decent error message if the platform
// on which the daemon is running does not support a builder command.
if err := platformSupports(strings.ToLower(cmd)); err != nil {
return err
}
attrs := ast.Attributes
original := ast.Original
flags := ast.Flags
strList := []string{}
msg := fmt.Sprintf("Step %d/%d : %s", stepN+1, stepTotal, upperCasedCmd)
if len(ast.Flags) > 0 {
msg += " " + strings.Join(ast.Flags, " ")
}
if cmd == "onbuild" {
if ast.Next == nil {
return fmt.Errorf("ONBUILD requires at least one argument")
}
ast = ast.Next.Children[0]
strList = append(strList, ast.Value)
msg += " " + ast.Value
if len(ast.Flags) > 0 {
msg += " " + strings.Join(ast.Flags, " ")
}
}
// count the number of nodes that we are going to traverse first
// so we can pre-create the argument and message array. This speeds up the
// allocation of those list a lot when they have a lot of arguments
cursor := ast
var n int
for cursor.Next != nil {
cursor = cursor.Next
n++
}
msgList := make([]string, n)
var i int
// Append the build-time args to config-environment.
// This allows builder config to override the variables, making the behavior similar to
// a shell script i.e. `ENV foo bar` overrides value of `foo` passed in build
// context. But `ENV foo $foo` will use the value from build context if one
// isn't already been defined by a previous ENV primitive.
// Note, we get this behavior because we know that ProcessWord() will
// stop on the first occurrence of a variable name and not notice
// a subsequent one. So, putting the buildArgs list after the Config.Env
// list, in 'envs', is safe.
envs := b.runConfig.Env
for key, val := range b.options.BuildArgs {
if !b.isBuildArgAllowed(key) {
// skip build-args that are not in allowed list, meaning they have
// not been defined by an "ARG" Dockerfile command yet.
// This is an error condition but only if there is no "ARG" in the entire
// Dockerfile, so we'll generate any necessary errors after we parsed
// the entire file (see 'leftoverArgs' processing in evaluator.go )
continue
}
envs = append(envs, fmt.Sprintf("%s=%s", key, *val))
}
for ast.Next != nil {
ast = ast.Next
var str string
str = ast.Value
if replaceEnvAllowed[cmd] {
var err error
var words []string
if allowWordExpansion[cmd] {
words, err = ProcessWords(str, envs, b.directive.EscapeToken)
if err != nil {
return err
}
strList = append(strList, words...)
} else {
str, err = ProcessWord(str, envs, b.directive.EscapeToken)
if err != nil {
return err
}
strList = append(strList, str)
}
} else {
strList = append(strList, str)
}
msgList[i] = ast.Value
i++
}
msg += " " + strings.Join(msgList, " ")
fmt.Fprintln(b.Stdout, msg)
// XXX yes, we skip any cmds that are not valid; the parser should have
// picked these out already.
if f, ok := evaluateTable[cmd]; ok {
b.flags = NewBFlags()
b.flags.Args = flags
return f(b, strList, attrs, original)
}
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
}
// checkDispatch does a simple check for syntax errors of the Dockerfile.
// Because some of the instructions can only be validated through runtime,
// arg, env, etc., this syntax check will not be complete and could not replace
// the runtime check. Instead, this function is only a helper that allows
// user to find out the obvious error in Dockerfile earlier on.
// onbuild bool: indicate if instruction XXX is part of `ONBUILD XXX` trigger
func (b *Builder) checkDispatch(ast *parser.Node, onbuild bool) error {
cmd := ast.Value
upperCasedCmd := strings.ToUpper(cmd)
// To ensure the user is given a decent error message if the platform
// on which the daemon is running does not support a builder command.
if err := platformSupports(strings.ToLower(cmd)); err != nil {
return err
}
// The instruction itself is ONBUILD, we will make sure it follows with at
// least one argument
if upperCasedCmd == "ONBUILD" {
if ast.Next == nil {
return fmt.Errorf("ONBUILD requires at least one argument")
}
}
// The instruction is part of ONBUILD trigger (not the instruction itself)
if onbuild {
switch upperCasedCmd {
case "ONBUILD":
return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
case "MAINTAINER", "FROM":
return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", upperCasedCmd)
}
}
if _, ok := evaluateTable[cmd]; ok {
return nil
}
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
}