Browse code

Some refactoring of dispatch()

Remove runConfig from Builder and dispatchRequest. It is not only on
dispatchState.

Move dispatch state fields from Builder to dispatchState

Move stageName tracking to dispatchRequest.

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2017/04/27 06:45:16
Showing 8 changed files
... ...
@@ -1,5 +1,10 @@
1 1
 package dockerfile
2 2
 
3
+import (
4
+	"fmt"
5
+	"github.com/docker/docker/runconfig/opts"
6
+)
7
+
3 8
 // builtinAllowedBuildArgs is list of built-in allowed build args
4 9
 // these args are considered transparent and are excluded from the image history.
5 10
 // Filtering from history is implemented in dispatchers.go
... ...
@@ -96,6 +101,19 @@ func (b *buildArgs) getAllFromMapping(source map[string]*string) map[string]stri
96 96
 	return m
97 97
 }
98 98
 
99
+// FilterAllowed returns all allowed args without the filtered args
100
+func (b *buildArgs) FilterAllowed(filter []string) []string {
101
+	envs := []string{}
102
+	configEnv := opts.ConvertKVStringsToMap(filter)
103
+
104
+	for key, val := range b.GetAllAllowed() {
105
+		if _, ok := configEnv[key]; !ok {
106
+			envs = append(envs, fmt.Sprintf("%s=%s", key, val))
107
+		}
108
+	}
109
+	return envs
110
+}
111
+
99 112
 func (b *buildArgs) getBuildArg(key string, mapping map[string]*string) (string, bool) {
100 113
 	defaultValue, exists := mapping[key]
101 114
 	// Return override from options if one is defined
... ...
@@ -95,20 +95,12 @@ type Builder struct {
95 95
 	source    builder.Source
96 96
 	clientCtx context.Context
97 97
 
98
-	runConfig     *container.Config // runconfig for cmd, run, entrypoint etc.
99 98
 	tmpContainers map[string]struct{}
100 99
 	imageContexts *imageContexts // helper for storing contexts from builds
101 100
 	disableCommit bool
102 101
 	cacheBusted   bool
103 102
 	buildArgs     *buildArgs
104 103
 	imageCache    builder.ImageCache
105
-
106
-	// TODO: these move to DispatchState
107
-	maintainer  string
108
-	cmdSet      bool
109
-	noBaseImage bool   // A flag to track the use of `scratch` as the base image
110
-	image       string // imageID
111
-	from        builder.Image
112 104
 }
113 105
 
114 106
 // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
... ...
@@ -124,7 +116,6 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
124 124
 		Stderr:        options.ProgressWriter.StderrFormatter,
125 125
 		Output:        options.ProgressWriter.Output,
126 126
 		docker:        options.Backend,
127
-		runConfig:     new(container.Config),
128 127
 		tmpContainers: map[string]struct{}{},
129 128
 		buildArgs:     newBuildArgs(config.BuildArgs),
130 129
 	}
... ...
@@ -136,7 +127,6 @@ func (b *Builder) resetImageCache() {
136 136
 	if icb, ok := b.docker.(builder.ImageCacheBuilder); ok {
137 137
 		b.imageCache = icb.MakeImageCache(b.options.CacheFrom)
138 138
 	}
139
-	b.noBaseImage = false
140 139
 	b.cacheBusted = false
141 140
 }
142 141
 
... ...
@@ -154,59 +144,61 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil
154 154
 		return nil, err
155 155
 	}
156 156
 
157
-	imageID, err := b.dispatchDockerfileWithCancellation(dockerfile)
157
+	dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile)
158 158
 	if err != nil {
159 159
 		return nil, err
160 160
 	}
161 161
 
162
+	if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
163
+		return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
164
+	}
165
+
162 166
 	b.warnOnUnusedBuildArgs()
163 167
 
164
-	if imageID == "" {
168
+	if dispatchState.imageID == "" {
165 169
 		return nil, errors.New("No image was generated. Is your Dockerfile empty?")
166 170
 	}
167
-	return &builder.Result{ImageID: imageID, FromImage: b.from}, nil
171
+	return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
168 172
 }
169 173
 
170
-func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (string, error) {
174
+func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (*dispatchState, error) {
171 175
 	shlex := NewShellLex(dockerfile.EscapeToken)
172
-
176
+	state := newDispatchState()
173 177
 	total := len(dockerfile.AST.Children)
174
-	var imageID string
178
+	var err error
175 179
 	for i, n := range dockerfile.AST.Children {
176 180
 		select {
177 181
 		case <-b.clientCtx.Done():
178 182
 			logrus.Debug("Builder: build cancelled!")
179 183
 			fmt.Fprint(b.Stdout, "Build cancelled")
180
-			return "", errors.New("Build cancelled")
184
+			return nil, errors.New("Build cancelled")
181 185
 		default:
182 186
 			// Not cancelled yet, keep going...
183 187
 		}
184 188
 
185
-		if command.From == n.Value && b.imageContexts.isCurrentTarget(b.options.Target) {
189
+		if n.Value == command.From && state.isCurrentStage(b.options.Target) {
186 190
 			break
187 191
 		}
188 192
 
189
-		if err := b.dispatch(i, total, n, shlex); err != nil {
193
+		opts := dispatchOptions{
194
+			state:   state,
195
+			stepMsg: formatStep(i, total),
196
+			node:    n,
197
+			shlex:   shlex,
198
+		}
199
+		if state, err = b.dispatch(opts); err != nil {
190 200
 			if b.options.ForceRemove {
191 201
 				b.clearTmp()
192 202
 			}
193
-			return "", err
203
+			return nil, err
194 204
 		}
195 205
 
196
-		// TODO: get this from dispatch
197
-		imageID = b.image
198
-
199
-		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(imageID))
206
+		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
200 207
 		if b.options.Remove {
201 208
 			b.clearTmp()
202 209
 		}
203 210
 	}
204
-
205
-	if b.options.Target != "" && !b.imageContexts.isCurrentTarget(b.options.Target) {
206
-		return "", errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
207
-	}
208
-
209
-	return imageID, nil
211
+	return state, nil
210 212
 }
211 213
 
212 214
 func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
... ...
@@ -227,12 +219,6 @@ func (b *Builder) warnOnUnusedBuildArgs() {
227 227
 	}
228 228
 }
229 229
 
230
-// hasFromImage returns true if the builder has processed a `FROM <image>` line
231
-// TODO: move to DispatchState
232
-func (b *Builder) hasFromImage() bool {
233
-	return b.image != "" || b.noBaseImage
234
-}
235
-
236 230
 // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
237 231
 // It will:
238 232
 // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
... ...
@@ -249,31 +235,28 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
249 249
 
250 250
 	b := newBuilder(context.Background(), builderOptions{})
251 251
 
252
-	result, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
252
+	dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
253 253
 	if err != nil {
254 254
 		return nil, err
255 255
 	}
256 256
 
257 257
 	// ensure that the commands are valid
258
-	for _, n := range result.AST.Children {
258
+	for _, n := range dockerfile.AST.Children {
259 259
 		if !validCommitCommands[n.Value] {
260 260
 			return nil, fmt.Errorf("%s is not a valid change command", n.Value)
261 261
 		}
262 262
 	}
263 263
 
264
-	b.runConfig = config
265 264
 	b.Stdout = ioutil.Discard
266 265
 	b.Stderr = ioutil.Discard
267 266
 	b.disableCommit = true
268 267
 
269
-	if err := checkDispatchDockerfile(result.AST); err != nil {
270
-		return nil, err
271
-	}
272
-
273
-	if err := dispatchFromDockerfile(b, result); err != nil {
268
+	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
274 269
 		return nil, err
275 270
 	}
276
-	return b.runConfig, nil
271
+	dispatchState := newDispatchState()
272
+	dispatchState.runConfig = config
273
+	return dispatchFromDockerfile(b, dockerfile, dispatchState)
277 274
 }
278 275
 
279 276
 func checkDispatchDockerfile(dockerfile *parser.Node) error {
... ...
@@ -285,15 +268,21 @@ func checkDispatchDockerfile(dockerfile *parser.Node) error {
285 285
 	return nil
286 286
 }
287 287
 
288
-func dispatchFromDockerfile(b *Builder, result *parser.Result) error {
288
+func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState) (*container.Config, error) {
289 289
 	shlex := NewShellLex(result.EscapeToken)
290 290
 	ast := result.AST
291 291
 	total := len(ast.Children)
292 292
 
293 293
 	for i, n := range ast.Children {
294
-		if err := b.dispatch(i, total, n, shlex); err != nil {
295
-			return err
294
+		opts := dispatchOptions{
295
+			state:   dispatchState,
296
+			stepMsg: formatStep(i, total),
297
+			node:    n,
298
+			shlex:   shlex,
299
+		}
300
+		if _, err := b.dispatch(opts); err != nil {
301
+			return nil, err
296 302
 		}
297 303
 	}
298
-	return nil
304
+	return dispatchState.runConfig, nil
299 305
 }
... ...
@@ -47,6 +47,7 @@ func env(req dispatchRequest) error {
47 47
 		return err
48 48
 	}
49 49
 
50
+	runConfig := req.state.runConfig
50 51
 	commitMessage := bytes.NewBufferString("ENV")
51 52
 
52 53
 	for j := 0; j < len(req.args); j += 2 {
... ...
@@ -59,21 +60,21 @@ func env(req dispatchRequest) error {
59 59
 		commitMessage.WriteString(" " + newVar)
60 60
 
61 61
 		gotOne := false
62
-		for i, envVar := range req.runConfig.Env {
62
+		for i, envVar := range runConfig.Env {
63 63
 			envParts := strings.SplitN(envVar, "=", 2)
64 64
 			compareFrom := envParts[0]
65 65
 			if equalEnvKeys(compareFrom, name) {
66
-				req.runConfig.Env[i] = newVar
66
+				runConfig.Env[i] = newVar
67 67
 				gotOne = true
68 68
 				break
69 69
 			}
70 70
 		}
71 71
 		if !gotOne {
72
-			req.runConfig.Env = append(req.runConfig.Env, newVar)
72
+			runConfig.Env = append(runConfig.Env, newVar)
73 73
 		}
74 74
 	}
75 75
 
76
-	return req.builder.commit(commitMessage.String())
76
+	return req.builder.commit(req.state, commitMessage.String())
77 77
 }
78 78
 
79 79
 // MAINTAINER some text <maybe@an.email.address>
... ...
@@ -89,8 +90,8 @@ func maintainer(req dispatchRequest) error {
89 89
 	}
90 90
 
91 91
 	maintainer := req.args[0]
92
-	req.builder.maintainer = maintainer
93
-	return req.builder.commit("MAINTAINER " + maintainer)
92
+	req.state.maintainer = maintainer
93
+	return req.builder.commit(req.state, "MAINTAINER "+maintainer)
94 94
 }
95 95
 
96 96
 // LABEL some json data describing the image
... ...
@@ -111,26 +112,25 @@ func label(req dispatchRequest) error {
111 111
 	}
112 112
 
113 113
 	commitStr := "LABEL"
114
+	runConfig := req.state.runConfig
114 115
 
115
-	if req.runConfig.Labels == nil {
116
-		req.runConfig.Labels = map[string]string{}
116
+	if runConfig.Labels == nil {
117
+		runConfig.Labels = map[string]string{}
117 118
 	}
118 119
 
119 120
 	for j := 0; j < len(req.args); j++ {
120
-		// name  ==> req.args[j]
121
-		// value ==> req.args[j+1]
122
-
123
-		if len(req.args[j]) == 0 {
121
+		name := req.args[j]
122
+		if name == "" {
124 123
 			return errBlankCommandNames("LABEL")
125 124
 		}
126 125
 
127
-		newVar := req.args[j] + "=" + req.args[j+1] + ""
128
-		commitStr += " " + newVar
126
+		value := req.args[j+1]
127
+		commitStr += " " + name + "=" + value
129 128
 
130
-		req.runConfig.Labels[req.args[j]] = req.args[j+1]
129
+		runConfig.Labels[name] = value
131 130
 		j++
132 131
 	}
133
-	return req.builder.commit(commitStr)
132
+	return req.builder.commit(req.state, commitStr)
134 133
 }
135 134
 
136 135
 // ADD foo /path
... ...
@@ -147,7 +147,7 @@ func add(req dispatchRequest) error {
147 147
 		return err
148 148
 	}
149 149
 
150
-	return req.builder.runContextCommand(req.args, true, true, "ADD", nil)
150
+	return req.builder.runContextCommand(req, true, true, "ADD", nil)
151 151
 }
152 152
 
153 153
 // COPY foo /path
... ...
@@ -174,13 +174,13 @@ func dispatchCopy(req dispatchRequest) error {
174 174
 		}
175 175
 	}
176 176
 
177
-	return req.builder.runContextCommand(req.args, false, false, "COPY", im)
177
+	return req.builder.runContextCommand(req, false, false, "COPY", im)
178 178
 }
179 179
 
180 180
 // FROM imagename[:tag | @digest] [AS build-stage-name]
181 181
 //
182 182
 func from(req dispatchRequest) error {
183
-	ctxName, err := parseBuildStageName(req.args)
183
+	stageName, err := parseBuildStageName(req.args)
184 184
 	if err != nil {
185 185
 		return err
186 186
 	}
... ...
@@ -190,21 +190,23 @@ func from(req dispatchRequest) error {
190 190
 	}
191 191
 
192 192
 	req.builder.resetImageCache()
193
-	if _, err := req.builder.imageContexts.add(ctxName); err != nil {
193
+	req.state.noBaseImage = false
194
+	req.state.stageName = stageName
195
+	if _, err := req.builder.imageContexts.add(stageName); err != nil {
194 196
 		return err
195 197
 	}
196 198
 
197
-	image, err := req.builder.getFromImage(req.shlex, req.args[0])
199
+	image, err := req.builder.getFromImage(req.state, req.shlex, req.args[0])
198 200
 	if err != nil {
199 201
 		return err
200 202
 	}
201 203
 	if image != nil {
202 204
 		req.builder.imageContexts.update(image.ImageID(), image.RunConfig())
203 205
 	}
204
-	req.builder.from = image
206
+	req.state.baseImage = image
205 207
 
206 208
 	req.builder.buildArgs.ResetAllowed()
207
-	return req.builder.processImageFrom(image)
209
+	return req.builder.processImageFrom(req.state, image)
208 210
 }
209 211
 
210 212
 func parseBuildStageName(args []string) (string, error) {
... ...
@@ -222,7 +224,7 @@ func parseBuildStageName(args []string) (string, error) {
222 222
 	return stageName, nil
223 223
 }
224 224
 
225
-func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
225
+func (b *Builder) getFromImage(dispatchState *dispatchState, shlex *ShellLex, name string) (builder.Image, error) {
226 226
 	substitutionArgs := []string{}
227 227
 	for key, value := range b.buildArgs.GetAllMeta() {
228 228
 		substitutionArgs = append(substitutionArgs, key+"="+value)
... ...
@@ -246,8 +248,8 @@ func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, err
246 246
 		if runtime.GOOS == "windows" {
247 247
 			return nil, errors.New("Windows does not support FROM scratch")
248 248
 		}
249
-		b.image = ""
250
-		b.noBaseImage = true
249
+		dispatchState.imageID = ""
250
+		dispatchState.noBaseImage = true
251 251
 		return nil, nil
252 252
 	}
253 253
 	return pullOrGetImage(b, name)
... ...
@@ -279,9 +281,10 @@ func onbuild(req dispatchRequest) error {
279 279
 		return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
280 280
 	}
281 281
 
282
+	runConfig := req.state.runConfig
282 283
 	original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
283
-	req.runConfig.OnBuild = append(req.runConfig.OnBuild, original)
284
-	return req.builder.commit("ONBUILD " + original)
284
+	runConfig.OnBuild = append(runConfig.OnBuild, original)
285
+	return req.builder.commit(req.state, "ONBUILD "+original)
285 286
 }
286 287
 
287 288
 // WORKDIR /tmp
... ...
@@ -298,9 +301,10 @@ func workdir(req dispatchRequest) error {
298 298
 		return err
299 299
 	}
300 300
 
301
+	runConfig := req.state.runConfig
301 302
 	// This is from the Dockerfile and will not necessarily be in platform
302 303
 	// specific semantics, hence ensure it is converted.
303
-	req.runConfig.WorkingDir, err = normaliseWorkdir(req.runConfig.WorkingDir, req.args[0])
304
+	runConfig.WorkingDir, err = normaliseWorkdir(runConfig.WorkingDir, req.args[0])
304 305
 	if err != nil {
305 306
 		return err
306 307
 	}
... ...
@@ -315,9 +319,9 @@ func workdir(req dispatchRequest) error {
315 315
 		return nil
316 316
 	}
317 317
 
318
-	comment := "WORKDIR " + req.runConfig.WorkingDir
319
-	runConfigWithCommentCmd := copyRunConfig(req.runConfig, withCmdCommentString(comment))
320
-	if hit, err := req.builder.probeCache(req.builder.image, runConfigWithCommentCmd); err != nil || hit {
318
+	comment := "WORKDIR " + runConfig.WorkingDir
319
+	runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment))
320
+	if hit, err := req.builder.probeCache(req.state, runConfigWithCommentCmd); err != nil || hit {
321 321
 		return err
322 322
 	}
323 323
 
... ...
@@ -334,7 +338,7 @@ func workdir(req dispatchRequest) error {
334 334
 		return err
335 335
 	}
336 336
 
337
-	return req.builder.commitContainer(container.ID, runConfigWithCommentCmd)
337
+	return req.builder.commitContainer(req.state, container.ID, runConfigWithCommentCmd)
338 338
 }
339 339
 
340 340
 // RUN some command yo
... ...
@@ -348,7 +352,7 @@ func workdir(req dispatchRequest) error {
348 348
 // RUN [ "echo", "hi" ] # echo hi
349 349
 //
350 350
 func run(req dispatchRequest) error {
351
-	if !req.builder.hasFromImage() {
351
+	if !req.state.hasFromImage() {
352 352
 		return errors.New("Please provide a source image with `from` prior to run")
353 353
 	}
354 354
 
... ...
@@ -356,29 +360,30 @@ func run(req dispatchRequest) error {
356 356
 		return err
357 357
 	}
358 358
 
359
+	stateRunConfig := req.state.runConfig
359 360
 	args := handleJSONArgs(req.args, req.attributes)
360 361
 	if !req.attributes["json"] {
361
-		args = append(getShell(req.runConfig), args...)
362
+		args = append(getShell(stateRunConfig), args...)
362 363
 	}
363 364
 	cmdFromArgs := strslice.StrSlice(args)
364
-	buildArgs := req.builder.buildArgsWithoutConfigEnv()
365
+	buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env)
365 366
 
366 367
 	saveCmd := cmdFromArgs
367 368
 	if len(buildArgs) > 0 {
368 369
 		saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs)
369 370
 	}
370 371
 
371
-	runConfigForCacheProbe := copyRunConfig(req.runConfig,
372
+	runConfigForCacheProbe := copyRunConfig(stateRunConfig,
372 373
 		withCmd(saveCmd),
373 374
 		withEntrypointOverride(saveCmd, nil))
374
-	hit, err := req.builder.probeCache(req.builder.image, runConfigForCacheProbe)
375
+	hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe)
375 376
 	if err != nil || hit {
376 377
 		return err
377 378
 	}
378 379
 
379
-	runConfig := copyRunConfig(req.runConfig,
380
+	runConfig := copyRunConfig(stateRunConfig,
380 381
 		withCmd(cmdFromArgs),
381
-		withEnv(append(req.runConfig.Env, buildArgs...)),
382
+		withEnv(append(stateRunConfig.Env, buildArgs...)),
382 383
 		withEntrypointOverride(saveCmd, strslice.StrSlice{""}))
383 384
 
384 385
 	// set config as already being escaped, this prevents double escaping on windows
... ...
@@ -393,7 +398,7 @@ func run(req dispatchRequest) error {
393 393
 		return err
394 394
 	}
395 395
 
396
-	return req.builder.commitContainer(cID, runConfigForCacheProbe)
396
+	return req.builder.commitContainer(req.state, cID, runConfigForCacheProbe)
397 397
 }
398 398
 
399 399
 // Derive the command to use for probeCache() and to commit in this container.
... ...
@@ -431,22 +436,22 @@ func cmd(req dispatchRequest) error {
431 431
 		return err
432 432
 	}
433 433
 
434
+	runConfig := req.state.runConfig
434 435
 	cmdSlice := handleJSONArgs(req.args, req.attributes)
435
-
436 436
 	if !req.attributes["json"] {
437
-		cmdSlice = append(getShell(req.runConfig), cmdSlice...)
437
+		cmdSlice = append(getShell(runConfig), cmdSlice...)
438 438
 	}
439 439
 
440
-	req.runConfig.Cmd = strslice.StrSlice(cmdSlice)
440
+	runConfig.Cmd = strslice.StrSlice(cmdSlice)
441 441
 	// set config as already being escaped, this prevents double escaping on windows
442
-	req.runConfig.ArgsEscaped = true
442
+	runConfig.ArgsEscaped = true
443 443
 
444
-	if err := req.builder.commit(fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
444
+	if err := req.builder.commit(req.state, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
445 445
 		return err
446 446
 	}
447 447
 
448 448
 	if len(req.args) != 0 {
449
-		req.builder.cmdSet = true
449
+		req.state.cmdSet = true
450 450
 	}
451 451
 
452 452
 	return nil
... ...
@@ -478,6 +483,7 @@ func healthcheck(req dispatchRequest) error {
478 478
 	if len(req.args) == 0 {
479 479
 		return errAtLeastOneArgument("HEALTHCHECK")
480 480
 	}
481
+	runConfig := req.state.runConfig
481 482
 	typ := strings.ToUpper(req.args[0])
482 483
 	args := req.args[1:]
483 484
 	if typ == "NONE" {
... ...
@@ -485,12 +491,12 @@ func healthcheck(req dispatchRequest) error {
485 485
 			return errors.New("HEALTHCHECK NONE takes no arguments")
486 486
 		}
487 487
 		test := strslice.StrSlice{typ}
488
-		req.runConfig.Healthcheck = &container.HealthConfig{
488
+		runConfig.Healthcheck = &container.HealthConfig{
489 489
 			Test: test,
490 490
 		}
491 491
 	} else {
492
-		if req.runConfig.Healthcheck != nil {
493
-			oldCmd := req.runConfig.Healthcheck.Test
492
+		if runConfig.Healthcheck != nil {
493
+			oldCmd := runConfig.Healthcheck.Test
494 494
 			if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
495 495
 				fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd)
496 496
 			}
... ...
@@ -554,10 +560,10 @@ func healthcheck(req dispatchRequest) error {
554 554
 			healthcheck.Retries = 0
555 555
 		}
556 556
 
557
-		req.runConfig.Healthcheck = &healthcheck
557
+		runConfig.Healthcheck = &healthcheck
558 558
 	}
559 559
 
560
-	return req.builder.commit(fmt.Sprintf("HEALTHCHECK %q", req.runConfig.Healthcheck))
560
+	return req.builder.commit(req.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck))
561 561
 }
562 562
 
563 563
 // ENTRYPOINT /usr/sbin/nginx
... ...
@@ -573,27 +579,28 @@ func entrypoint(req dispatchRequest) error {
573 573
 		return err
574 574
 	}
575 575
 
576
+	runConfig := req.state.runConfig
576 577
 	parsed := handleJSONArgs(req.args, req.attributes)
577 578
 
578 579
 	switch {
579 580
 	case req.attributes["json"]:
580 581
 		// ENTRYPOINT ["echo", "hi"]
581
-		req.runConfig.Entrypoint = strslice.StrSlice(parsed)
582
+		runConfig.Entrypoint = strslice.StrSlice(parsed)
582 583
 	case len(parsed) == 0:
583 584
 		// ENTRYPOINT []
584
-		req.runConfig.Entrypoint = nil
585
+		runConfig.Entrypoint = nil
585 586
 	default:
586 587
 		// ENTRYPOINT echo hi
587
-		req.runConfig.Entrypoint = strslice.StrSlice(append(getShell(req.runConfig), parsed[0]))
588
+		runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig), parsed[0]))
588 589
 	}
589 590
 
590 591
 	// when setting the entrypoint if a CMD was not explicitly set then
591 592
 	// set the command to nil
592
-	if !req.builder.cmdSet {
593
-		req.runConfig.Cmd = nil
593
+	if !req.state.cmdSet {
594
+		runConfig.Cmd = nil
594 595
 	}
595 596
 
596
-	return req.builder.commit(fmt.Sprintf("ENTRYPOINT %q", req.runConfig.Entrypoint))
597
+	return req.builder.commit(req.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint))
597 598
 }
598 599
 
599 600
 // EXPOSE 6667/tcp 7000/tcp
... ...
@@ -612,8 +619,9 @@ func expose(req dispatchRequest) error {
612 612
 		return err
613 613
 	}
614 614
 
615
-	if req.runConfig.ExposedPorts == nil {
616
-		req.runConfig.ExposedPorts = make(nat.PortSet)
615
+	runConfig := req.state.runConfig
616
+	if runConfig.ExposedPorts == nil {
617
+		runConfig.ExposedPorts = make(nat.PortSet)
617 618
 	}
618 619
 
619 620
 	ports, _, err := nat.ParsePortSpecs(portsTab)
... ...
@@ -627,14 +635,14 @@ func expose(req dispatchRequest) error {
627 627
 	portList := make([]string, len(ports))
628 628
 	var i int
629 629
 	for port := range ports {
630
-		if _, exists := req.runConfig.ExposedPorts[port]; !exists {
631
-			req.runConfig.ExposedPorts[port] = struct{}{}
630
+		if _, exists := runConfig.ExposedPorts[port]; !exists {
631
+			runConfig.ExposedPorts[port] = struct{}{}
632 632
 		}
633 633
 		portList[i] = string(port)
634 634
 		i++
635 635
 	}
636 636
 	sort.Strings(portList)
637
-	return req.builder.commit("EXPOSE " + strings.Join(portList, " "))
637
+	return req.builder.commit(req.state, "EXPOSE "+strings.Join(portList, " "))
638 638
 }
639 639
 
640 640
 // USER foo
... ...
@@ -651,8 +659,8 @@ func user(req dispatchRequest) error {
651 651
 		return err
652 652
 	}
653 653
 
654
-	req.runConfig.User = req.args[0]
655
-	return req.builder.commit(fmt.Sprintf("USER %v", req.args))
654
+	req.state.runConfig.User = req.args[0]
655
+	return req.builder.commit(req.state, fmt.Sprintf("USER %v", req.args))
656 656
 }
657 657
 
658 658
 // VOLUME /foo
... ...
@@ -668,17 +676,18 @@ func volume(req dispatchRequest) error {
668 668
 		return err
669 669
 	}
670 670
 
671
-	if req.runConfig.Volumes == nil {
672
-		req.runConfig.Volumes = map[string]struct{}{}
671
+	runConfig := req.state.runConfig
672
+	if runConfig.Volumes == nil {
673
+		runConfig.Volumes = map[string]struct{}{}
673 674
 	}
674 675
 	for _, v := range req.args {
675 676
 		v = strings.TrimSpace(v)
676 677
 		if v == "" {
677 678
 			return errors.New("VOLUME specified can not be an empty string")
678 679
 		}
679
-		req.runConfig.Volumes[v] = struct{}{}
680
+		runConfig.Volumes[v] = struct{}{}
680 681
 	}
681
-	return req.builder.commit(fmt.Sprintf("VOLUME %v", req.args))
682
+	return req.builder.commit(req.state, fmt.Sprintf("VOLUME %v", req.args))
682 683
 }
683 684
 
684 685
 // STOPSIGNAL signal
... ...
@@ -695,8 +704,8 @@ func stopSignal(req dispatchRequest) error {
695 695
 		return err
696 696
 	}
697 697
 
698
-	req.runConfig.StopSignal = sig
699
-	return req.builder.commit(fmt.Sprintf("STOPSIGNAL %v", req.args))
698
+	req.state.runConfig.StopSignal = sig
699
+	return req.builder.commit(req.state, fmt.Sprintf("STOPSIGNAL %v", req.args))
700 700
 }
701 701
 
702 702
 // ARG name[=value]
... ...
@@ -742,11 +751,11 @@ func arg(req dispatchRequest) error {
742 742
 	req.builder.buildArgs.AddArg(name, value)
743 743
 
744 744
 	// Arg before FROM doesn't add a layer
745
-	if !req.builder.hasFromImage() {
745
+	if !req.state.hasFromImage() {
746 746
 		req.builder.buildArgs.AddMetaArg(name, value)
747 747
 		return nil
748 748
 	}
749
-	return req.builder.commit("ARG " + arg)
749
+	return req.builder.commit(req.state, "ARG "+arg)
750 750
 }
751 751
 
752 752
 // SHELL powershell -command
... ...
@@ -763,12 +772,12 @@ func shell(req dispatchRequest) error {
763 763
 		return errAtLeastOneArgument("SHELL")
764 764
 	case req.attributes["json"]:
765 765
 		// SHELL ["powershell", "-command"]
766
-		req.runConfig.Shell = strslice.StrSlice(shellSlice)
766
+		req.state.runConfig.Shell = strslice.StrSlice(shellSlice)
767 767
 	default:
768 768
 		// SHELL powershell -command - not JSON
769 769
 		return errNotJSON("SHELL", req.original)
770 770
 	}
771
-	return req.builder.commit(fmt.Sprintf("SHELL %v", shellSlice))
771
+	return req.builder.commit(req.state, fmt.Sprintf("SHELL %v", shellSlice))
772 772
 }
773 773
 
774 774
 func errAtLeastOneArgument(command string) error {
... ...
@@ -26,7 +26,7 @@ type commandWithFunction struct {
26 26
 
27 27
 func withArgs(f dispatcher) func([]string) error {
28 28
 	return func(args []string) error {
29
-		return f(dispatchRequest{args: args, runConfig: &container.Config{}})
29
+		return f(dispatchRequest{args: args})
30 30
 	}
31 31
 }
32 32
 
... ...
@@ -38,17 +38,16 @@ func withBuilderAndArgs(builder *Builder, f dispatcher) func([]string) error {
38 38
 
39 39
 func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest {
40 40
 	return dispatchRequest{
41
-		builder:   builder,
42
-		args:      args,
43
-		flags:     NewBFlags(),
44
-		runConfig: &container.Config{},
45
-		shlex:     NewShellLex(parser.DefaultEscapeToken),
41
+		builder: builder,
42
+		args:    args,
43
+		flags:   NewBFlags(),
44
+		shlex:   NewShellLex(parser.DefaultEscapeToken),
45
+		state:   &dispatchState{runConfig: &container.Config{}},
46 46
 	}
47 47
 }
48 48
 
49 49
 func newBuilderWithMockBackend() *Builder {
50 50
 	b := &Builder{
51
-		runConfig:     &container.Config{},
52 51
 		options:       &types.ImageBuildOptions{},
53 52
 		docker:        &MockBackend{},
54 53
 		buildArgs:     newBuildArgs(make(map[string]*string)),
... ...
@@ -138,7 +137,7 @@ func TestEnv2Variables(t *testing.T) {
138 138
 		fmt.Sprintf("%s=%s", args[0], args[1]),
139 139
 		fmt.Sprintf("%s=%s", args[2], args[3]),
140 140
 	}
141
-	assert.Equal(t, expected, req.runConfig.Env)
141
+	assert.Equal(t, expected, req.state.runConfig.Env)
142 142
 }
143 143
 
144 144
 func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
... ...
@@ -146,7 +145,7 @@ func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
146 146
 
147 147
 	args := []string{"var1", "val1"}
148 148
 	req := defaultDispatchReq(b, args...)
149
-	req.runConfig.Env = []string{"var1=old", "var2=fromenv"}
149
+	req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
150 150
 	err := env(req)
151 151
 	require.NoError(t, err)
152 152
 
... ...
@@ -154,16 +153,17 @@ func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
154 154
 		fmt.Sprintf("%s=%s", args[0], args[1]),
155 155
 		"var2=fromenv",
156 156
 	}
157
-	assert.Equal(t, expected, req.runConfig.Env)
157
+	assert.Equal(t, expected, req.state.runConfig.Env)
158 158
 }
159 159
 
160 160
 func TestMaintainer(t *testing.T) {
161 161
 	maintainerEntry := "Some Maintainer <maintainer@example.com>"
162 162
 
163 163
 	b := newBuilderWithMockBackend()
164
-	err := maintainer(defaultDispatchReq(b, maintainerEntry))
164
+	req := defaultDispatchReq(b, maintainerEntry)
165
+	err := maintainer(req)
165 166
 	require.NoError(t, err)
166
-	assert.Equal(t, maintainerEntry, b.maintainer)
167
+	assert.Equal(t, maintainerEntry, req.state.maintainer)
167 168
 }
168 169
 
169 170
 func TestLabel(t *testing.T) {
... ...
@@ -176,13 +176,14 @@ func TestLabel(t *testing.T) {
176 176
 	err := label(req)
177 177
 	require.NoError(t, err)
178 178
 
179
-	require.Contains(t, req.runConfig.Labels, labelName)
180
-	assert.Equal(t, req.runConfig.Labels[labelName], labelValue)
179
+	require.Contains(t, req.state.runConfig.Labels, labelName)
180
+	assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue)
181 181
 }
182 182
 
183 183
 func TestFromScratch(t *testing.T) {
184 184
 	b := newBuilderWithMockBackend()
185
-	err := from(defaultDispatchReq(b, "scratch"))
185
+	req := defaultDispatchReq(b, "scratch")
186
+	err := from(req)
186 187
 
187 188
 	if runtime.GOOS == "windows" {
188 189
 		assert.EqualError(t, err, "Windows does not support FROM scratch")
... ...
@@ -190,8 +191,8 @@ func TestFromScratch(t *testing.T) {
190 190
 	}
191 191
 
192 192
 	require.NoError(t, err)
193
-	assert.Equal(t, "", b.image)
194
-	assert.Equal(t, true, b.noBaseImage)
193
+	assert.Equal(t, "", req.state.imageID)
194
+	assert.Equal(t, true, req.state.noBaseImage)
195 195
 }
196 196
 
197 197
 func TestFromWithArg(t *testing.T) {
... ...
@@ -205,11 +206,12 @@ func TestFromWithArg(t *testing.T) {
205 205
 	b.docker.(*MockBackend).getImageOnBuildFunc = getImage
206 206
 
207 207
 	require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag)))
208
-	err := from(defaultDispatchReq(b, "alpine${THETAG}"))
208
+	req := defaultDispatchReq(b, "alpine${THETAG}")
209
+	err := from(req)
209 210
 
210 211
 	require.NoError(t, err)
211
-	assert.Equal(t, expected, b.image)
212
-	assert.Equal(t, expected, b.from.ImageID())
212
+	assert.Equal(t, expected, req.state.imageID)
213
+	assert.Equal(t, expected, req.state.baseImage.ImageID())
213 214
 	assert.Len(t, b.buildArgs.GetAllAllowed(), 0)
214 215
 	assert.Len(t, b.buildArgs.GetAllMeta(), 1)
215 216
 }
... ...
@@ -225,9 +227,10 @@ func TestFromWithUndefinedArg(t *testing.T) {
225 225
 	b.docker.(*MockBackend).getImageOnBuildFunc = getImage
226 226
 	b.options.BuildArgs = map[string]*string{"THETAG": &tag}
227 227
 
228
-	err := from(defaultDispatchReq(b, "alpine${THETAG}"))
228
+	req := defaultDispatchReq(b, "alpine${THETAG}")
229
+	err := from(req)
229 230
 	require.NoError(t, err)
230
-	assert.Equal(t, expected, b.image)
231
+	assert.Equal(t, expected, req.state.imageID)
231 232
 }
232 233
 
233 234
 func TestOnbuildIllegalTriggers(t *testing.T) {
... ...
@@ -249,11 +252,11 @@ func TestOnbuild(t *testing.T) {
249 249
 
250 250
 	req := defaultDispatchReq(b, "ADD", ".", "/app/src")
251 251
 	req.original = "ONBUILD ADD . /app/src"
252
-	req.runConfig = &container.Config{}
252
+	req.state.runConfig = &container.Config{}
253 253
 
254 254
 	err := onbuild(req)
255 255
 	require.NoError(t, err)
256
-	assert.Equal(t, "ADD . /app/src", req.runConfig.OnBuild[0])
256
+	assert.Equal(t, "ADD . /app/src", req.state.runConfig.OnBuild[0])
257 257
 }
258 258
 
259 259
 func TestWorkdir(t *testing.T) {
... ...
@@ -266,7 +269,7 @@ func TestWorkdir(t *testing.T) {
266 266
 	req := defaultDispatchReq(b, workingDir)
267 267
 	err := workdir(req)
268 268
 	require.NoError(t, err)
269
-	assert.Equal(t, workingDir, req.runConfig.WorkingDir)
269
+	assert.Equal(t, workingDir, req.state.runConfig.WorkingDir)
270 270
 }
271 271
 
272 272
 func TestCmd(t *testing.T) {
... ...
@@ -284,8 +287,8 @@ func TestCmd(t *testing.T) {
284 284
 		expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command))
285 285
 	}
286 286
 
287
-	assert.Equal(t, expectedCommand, req.runConfig.Cmd)
288
-	assert.True(t, b.cmdSet)
287
+	assert.Equal(t, expectedCommand, req.state.runConfig.Cmd)
288
+	assert.True(t, req.state.cmdSet)
289 289
 }
290 290
 
291 291
 func TestHealthcheckNone(t *testing.T) {
... ...
@@ -295,8 +298,8 @@ func TestHealthcheckNone(t *testing.T) {
295 295
 	err := healthcheck(req)
296 296
 	require.NoError(t, err)
297 297
 
298
-	require.NotNil(t, req.runConfig.Healthcheck)
299
-	assert.Equal(t, []string{"NONE"}, req.runConfig.Healthcheck.Test)
298
+	require.NotNil(t, req.state.runConfig.Healthcheck)
299
+	assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test)
300 300
 }
301 301
 
302 302
 func TestHealthcheckCmd(t *testing.T) {
... ...
@@ -307,9 +310,9 @@ func TestHealthcheckCmd(t *testing.T) {
307 307
 	err := healthcheck(req)
308 308
 	require.NoError(t, err)
309 309
 
310
-	require.NotNil(t, req.runConfig.Healthcheck)
310
+	require.NotNil(t, req.state.runConfig.Healthcheck)
311 311
 	expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
312
-	assert.Equal(t, expectedTest, req.runConfig.Healthcheck.Test)
312
+	assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test)
313 313
 }
314 314
 
315 315
 func TestEntrypoint(t *testing.T) {
... ...
@@ -319,7 +322,7 @@ func TestEntrypoint(t *testing.T) {
319 319
 	req := defaultDispatchReq(b, entrypointCmd)
320 320
 	err := entrypoint(req)
321 321
 	require.NoError(t, err)
322
-	require.NotNil(t, req.runConfig.Entrypoint)
322
+	require.NotNil(t, req.state.runConfig.Entrypoint)
323 323
 
324 324
 	var expectedEntrypoint strslice.StrSlice
325 325
 	if runtime.GOOS == "windows" {
... ...
@@ -327,7 +330,7 @@ func TestEntrypoint(t *testing.T) {
327 327
 	} else {
328 328
 		expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd))
329 329
 	}
330
-	assert.Equal(t, expectedEntrypoint, req.runConfig.Entrypoint)
330
+	assert.Equal(t, expectedEntrypoint, req.state.runConfig.Entrypoint)
331 331
 }
332 332
 
333 333
 func TestExpose(t *testing.T) {
... ...
@@ -338,12 +341,12 @@ func TestExpose(t *testing.T) {
338 338
 	err := expose(req)
339 339
 	require.NoError(t, err)
340 340
 
341
-	require.NotNil(t, req.runConfig.ExposedPorts)
342
-	require.Len(t, req.runConfig.ExposedPorts, 1)
341
+	require.NotNil(t, req.state.runConfig.ExposedPorts)
342
+	require.Len(t, req.state.runConfig.ExposedPorts, 1)
343 343
 
344 344
 	portsMapping, err := nat.ParsePortSpec(exposedPort)
345 345
 	require.NoError(t, err)
346
-	assert.Contains(t, req.runConfig.ExposedPorts, portsMapping[0].Port)
346
+	assert.Contains(t, req.state.runConfig.ExposedPorts, portsMapping[0].Port)
347 347
 }
348 348
 
349 349
 func TestUser(t *testing.T) {
... ...
@@ -353,7 +356,7 @@ func TestUser(t *testing.T) {
353 353
 	req := defaultDispatchReq(b, userCommand)
354 354
 	err := user(req)
355 355
 	require.NoError(t, err)
356
-	assert.Equal(t, userCommand, req.runConfig.User)
356
+	assert.Equal(t, userCommand, req.state.runConfig.User)
357 357
 }
358 358
 
359 359
 func TestVolume(t *testing.T) {
... ...
@@ -365,9 +368,9 @@ func TestVolume(t *testing.T) {
365 365
 	err := volume(req)
366 366
 	require.NoError(t, err)
367 367
 
368
-	require.NotNil(t, req.runConfig.Volumes)
369
-	assert.Len(t, req.runConfig.Volumes, 1)
370
-	assert.Contains(t, req.runConfig.Volumes, exposedVolume)
368
+	require.NotNil(t, req.state.runConfig.Volumes)
369
+	assert.Len(t, req.state.runConfig.Volumes, 1)
370
+	assert.Contains(t, req.state.runConfig.Volumes, exposedVolume)
371 371
 }
372 372
 
373 373
 func TestStopSignal(t *testing.T) {
... ...
@@ -377,7 +380,7 @@ func TestStopSignal(t *testing.T) {
377 377
 	req := defaultDispatchReq(b, signal)
378 378
 	err := stopSignal(req)
379 379
 	require.NoError(t, err)
380
-	assert.Equal(t, signal, req.runConfig.StopSignal)
380
+	assert.Equal(t, signal, req.state.runConfig.StopSignal)
381 381
 }
382 382
 
383 383
 func TestArg(t *testing.T) {
... ...
@@ -405,7 +408,7 @@ func TestShell(t *testing.T) {
405 405
 	require.NoError(t, err)
406 406
 
407 407
 	expectedShell := strslice.StrSlice([]string{shellCmd})
408
-	assert.Equal(t, expectedShell, req.runConfig.Shell)
408
+	assert.Equal(t, expectedShell, req.state.runConfig.Shell)
409 409
 }
410 410
 
411 411
 func TestParseOptInterval(t *testing.T) {
... ...
@@ -439,8 +442,9 @@ func TestRunWithBuildArgs(t *testing.T) {
439 439
 	b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
440 440
 	b.disableCommit = false
441 441
 
442
+	runConfig := &container.Config{}
442 443
 	origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
443
-	cmdWithShell := strslice.StrSlice(append(getShell(b.runConfig), "echo foo"))
444
+	cmdWithShell := strslice.StrSlice(append(getShell(runConfig), "echo foo"))
444 445
 	envVars := []string{"|1", "one=two"}
445 446
 	cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...))
446 447
 
... ...
@@ -477,12 +481,10 @@ func TestRunWithBuildArgs(t *testing.T) {
477 477
 	req := defaultDispatchReq(b, "abcdef")
478 478
 	require.NoError(t, from(req))
479 479
 	b.buildArgs.AddArg("one", strPtr("two"))
480
-	// TODO: this can be removed with b.runConfig
481
-	req.runConfig.Cmd = origCmd
482 480
 
483 481
 	req.args = []string{"echo foo"}
484 482
 	require.NoError(t, run(req))
485 483
 
486 484
 	// Check that runConfig.Cmd has not been modified by run
487
-	assert.Equal(t, origCmd, b.runConfig.Cmd)
485
+	assert.Equal(t, origCmd, req.state.runConfig.Cmd)
488 486
 }
... ...
@@ -25,6 +25,7 @@ import (
25 25
 	"strings"
26 26
 
27 27
 	"github.com/docker/docker/api/types/container"
28
+	"github.com/docker/docker/builder"
28 29
 	"github.com/docker/docker/builder/dockerfile/command"
29 30
 	"github.com/docker/docker/builder/dockerfile/parser"
30 31
 	"github.com/docker/docker/runconfig/opts"
... ...
@@ -64,19 +65,19 @@ type dispatchRequest struct {
64 64
 	attributes map[string]bool
65 65
 	flags      *BFlags
66 66
 	original   string
67
-	runConfig  *container.Config
68 67
 	shlex      *ShellLex
68
+	state      *dispatchState
69 69
 }
70 70
 
71
-func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []string, shlex *ShellLex) dispatchRequest {
71
+func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest {
72 72
 	return dispatchRequest{
73 73
 		builder:    builder,
74 74
 		args:       args,
75
-		attributes: node.Attributes,
76
-		original:   node.Original,
77
-		flags:      NewBFlagsWithArgs(node.Flags),
78
-		runConfig:  builder.runConfig,
79
-		shlex:      shlex,
75
+		attributes: options.node.Attributes,
76
+		original:   options.node.Original,
77
+		flags:      NewBFlagsWithArgs(options.node.Flags),
78
+		shlex:      options.shlex,
79
+		state:      options.state,
80 80
 	}
81 81
 }
82 82
 
... ...
@@ -107,6 +108,10 @@ func init() {
107 107
 	}
108 108
 }
109 109
 
110
+func formatStep(stepN int, stepTotal int) string {
111
+	return fmt.Sprintf("%d/%d", stepN+1, stepTotal)
112
+}
113
+
110 114
 // This method is the entrypoint to all statement handling routines.
111 115
 //
112 116
 // Almost all nodes will have this structure:
... ...
@@ -121,117 +126,144 @@ func init() {
121 121
 // such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
122 122
 // deal with that, at least until it becomes more of a general concern with new
123 123
 // features.
124
-func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node, shlex *ShellLex) error {
124
+func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
125
+	node := options.node
125 126
 	cmd := node.Value
126 127
 	upperCasedCmd := strings.ToUpper(cmd)
127 128
 
128 129
 	// To ensure the user is given a decent error message if the platform
129 130
 	// on which the daemon is running does not support a builder command.
130 131
 	if err := platformSupports(strings.ToLower(cmd)); err != nil {
131
-		return err
132
+		return nil, err
132 133
 	}
133 134
 
134
-	strList := []string{}
135
-	msg := bytes.NewBufferString(fmt.Sprintf("Step %d/%d : %s", stepN+1, stepTotal, upperCasedCmd))
136
-
137
-	if len(node.Flags) > 0 {
138
-		msg.WriteString(strings.Join(node.Flags, " "))
139
-	}
135
+	msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
136
+		options.stepMsg, upperCasedCmd, formatFlags(node.Flags)))
140 137
 
138
+	args := []string{}
141 139
 	ast := node
142
-	if cmd == "onbuild" {
143
-		if ast.Next == nil {
144
-			return errors.New("ONBUILD requires at least one argument")
140
+	if cmd == command.Onbuild {
141
+		var err error
142
+		ast, args, err = handleOnBuildNode(node, msg)
143
+		if err != nil {
144
+			return nil, err
145 145
 		}
146
-		ast = ast.Next.Children[0]
147
-		strList = append(strList, ast.Value)
148
-		msg.WriteString(" " + ast.Value)
146
+	}
149 147
 
150
-		if len(ast.Flags) > 0 {
151
-			msg.WriteString(" " + strings.Join(ast.Flags, " "))
152
-		}
148
+	runConfigEnv := options.state.runConfig.Env
149
+	envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...)
150
+	processFunc := createProcessWordFunc(options.shlex, cmd, envs)
151
+	words, err := getDispatchArgsFromNode(ast, processFunc, msg)
152
+	if err != nil {
153
+		return nil, err
153 154
 	}
155
+	args = append(args, words...)
154 156
 
155
-	msgList := initMsgList(ast)
156
-	// Append build args to runConfig environment variables
157
-	envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
157
+	fmt.Fprintln(b.Stdout, msg.String())
158 158
 
159
-	processFunc := getProcessFunc(shlex, cmd)
160
-	for i := 0; ast.Next != nil; i++ {
161
-		ast = ast.Next
162
-		words, err := processFunc(ast.Value, envs)
163
-		if err != nil {
164
-			return err
165
-		}
166
-		strList = append(strList, words...)
167
-		msgList[i] = ast.Value
159
+	f, ok := evaluateTable[cmd]
160
+	if !ok {
161
+		return nil, fmt.Errorf("unknown instruction: %s", upperCasedCmd)
168 162
 	}
163
+	if err := f(newDispatchRequestFromOptions(options, b, args)); err != nil {
164
+		return nil, err
165
+	}
166
+	options.state.updateRunConfig()
167
+	return options.state, nil
168
+}
169 169
 
170
-	msg.WriteString(" " + strings.Join(msgList, " "))
171
-	fmt.Fprintln(b.Stdout, msg.String())
170
+type dispatchOptions struct {
171
+	state   *dispatchState
172
+	stepMsg string
173
+	node    *parser.Node
174
+	shlex   *ShellLex
175
+}
172 176
 
173
-	// XXX yes, we skip any cmds that are not valid; the parser should have
174
-	// picked these out already.
175
-	if f, ok := evaluateTable[cmd]; ok {
176
-		if err := f(newDispatchRequestFromNode(node, b, strList, shlex)); err != nil {
177
-			return err
178
-		}
179
-		// TODO: return an object instead of setting things on builder
180
-		// If the step created a new image set it as the imageID for the
181
-		// current runConfig
182
-		b.runConfig.Image = b.image
183
-		return nil
177
+// dispatchState is a data object which is modified by dispatchers
178
+type dispatchState struct {
179
+	runConfig   *container.Config
180
+	maintainer  string
181
+	cmdSet      bool
182
+	noBaseImage bool
183
+	imageID     string
184
+	baseImage   builder.Image
185
+	stageName   string
186
+}
187
+
188
+func newDispatchState() *dispatchState {
189
+	return &dispatchState{runConfig: &container.Config{}}
190
+}
191
+
192
+func (r *dispatchState) updateRunConfig() {
193
+	r.runConfig.Image = r.imageID
194
+}
195
+
196
+// hasFromImage returns true if the builder has processed a `FROM <image>` line
197
+func (r *dispatchState) hasFromImage() bool {
198
+	return r.imageID != "" || r.noBaseImage
199
+}
200
+
201
+func (r *dispatchState) runConfigEnvMapping() map[string]string {
202
+	return opts.ConvertKVStringsToMap(r.runConfig.Env)
203
+}
204
+
205
+func (r *dispatchState) isCurrentStage(target string) bool {
206
+	if target == "" {
207
+		return false
184 208
 	}
209
+	return strings.EqualFold(r.stageName, target)
210
+}
185 211
 
186
-	return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
212
+func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
213
+	if ast.Next == nil {
214
+		return nil, nil, errors.New("ONBUILD requires at least one argument")
215
+	}
216
+	ast = ast.Next.Children[0]
217
+	msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
218
+	return ast, []string{ast.Value}, nil
187 219
 }
188 220
 
189
-// count the number of nodes that we are going to traverse first
190
-// allocation of those list a lot when they have a lot of arguments
191
-func initMsgList(cursor *parser.Node) []string {
192
-	var n int
193
-	for ; cursor.Next != nil; n++ {
194
-		cursor = cursor.Next
221
+func formatFlags(flags []string) string {
222
+	if len(flags) > 0 {
223
+		return " " + strings.Join(flags, " ")
195 224
 	}
196
-	return make([]string, n)
225
+	return ""
197 226
 }
198 227
 
199
-type processFunc func(string, []string) ([]string, error)
228
+func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) {
229
+	args := []string{}
230
+	for i := 0; ast.Next != nil; i++ {
231
+		ast = ast.Next
232
+		words, err := processFunc(ast.Value)
233
+		if err != nil {
234
+			return nil, err
235
+		}
236
+		args = append(args, words...)
237
+		msg.WriteString(" " + ast.Value)
238
+	}
239
+	return args, nil
240
+}
200 241
 
201
-func getProcessFunc(shlex *ShellLex, cmd string) processFunc {
242
+type processWordFunc func(string) ([]string, error)
243
+
244
+func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
202 245
 	switch {
203 246
 	case !replaceEnvAllowed[cmd]:
204
-		return func(word string, _ []string) ([]string, error) {
247
+		return func(word string) ([]string, error) {
205 248
 			return []string{word}, nil
206 249
 		}
207 250
 	case allowWordExpansion[cmd]:
208
-		return shlex.ProcessWords
251
+		return func(word string) ([]string, error) {
252
+			return shlex.ProcessWords(word, envs)
253
+		}
209 254
 	default:
210
-		return func(word string, envs []string) ([]string, error) {
255
+		return func(word string) ([]string, error) {
211 256
 			word, err := shlex.ProcessWord(word, envs)
212 257
 			return []string{word}, err
213 258
 		}
214 259
 	}
215 260
 }
216 261
 
217
-// buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
218
-// args that are not overriden by runConfig environment variables.
219
-func (b *Builder) buildArgsWithoutConfigEnv() []string {
220
-	envs := []string{}
221
-	configEnv := b.runConfigEnvMapping()
222
-
223
-	for key, val := range b.buildArgs.GetAllAllowed() {
224
-		if _, ok := configEnv[key]; !ok {
225
-			envs = append(envs, fmt.Sprintf("%s=%s", key, val))
226
-		}
227
-	}
228
-	return envs
229
-}
230
-
231
-func (b *Builder) runConfigEnvMapping() map[string]string {
232
-	return opts.ConvertKVStringsToMap(b.runConfig.Env)
233
-}
234
-
235 262
 // checkDispatch does a simple check for syntax errors of the Dockerfile.
236 263
 // Because some of the instructions can only be validated through runtime,
237 264
 // arg, env, etc., this syntax check will not be complete and could not replace
... ...
@@ -123,7 +123,7 @@ func initDispatchTestCases() []dispatchTestCase {
123 123
 		{
124 124
 			name:          "Invalid instruction",
125 125
 			dockerfile:    `foo bar`,
126
-			expectedError: "Unknown instruction: FOO",
126
+			expectedError: "unknown instruction: FOO",
127 127
 			files:         nil,
128 128
 		}}
129 129
 
... ...
@@ -177,13 +177,11 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
177 177
 		t.Fatalf("Error when parsing Dockerfile: %s", err)
178 178
 	}
179 179
 
180
-	config := &container.Config{}
181 180
 	options := &types.ImageBuildOptions{
182 181
 		BuildArgs: make(map[string]*string),
183 182
 	}
184 183
 
185 184
 	b := &Builder{
186
-		runConfig: config,
187 185
 		options:   options,
188 186
 		Stdout:    ioutil.Discard,
189 187
 		source:    context,
... ...
@@ -192,7 +190,14 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
192 192
 
193 193
 	shlex := NewShellLex(parser.DefaultEscapeToken)
194 194
 	n := result.AST
195
-	err = b.dispatch(0, len(n.Children), n.Children[0], shlex)
195
+	state := &dispatchState{runConfig: &container.Config{}}
196
+	opts := dispatchOptions{
197
+		state:   state,
198
+		stepMsg: formatStep(0, len(n.Children)),
199
+		node:    n.Children[0],
200
+		shlex:   shlex,
201
+	}
202
+	state, err = b.dispatch(opts)
196 203
 
197 204
 	if err == nil {
198 205
 		t.Fatalf("No error when executing test %s", testCase.name)
... ...
@@ -19,11 +19,10 @@ type pathCache interface {
19 19
 // imageContexts is a helper for stacking up built image rootfs and reusing
20 20
 // them as contexts
21 21
 type imageContexts struct {
22
-	b           *Builder
23
-	list        []*imageMount
24
-	byName      map[string]*imageMount
25
-	cache       pathCache
26
-	currentName string
22
+	b      *Builder
23
+	list   []*imageMount
24
+	byName map[string]*imageMount
25
+	cache  pathCache
27 26
 }
28 27
 
29 28
 func (ic *imageContexts) newImageMount(id string) *imageMount {
... ...
@@ -41,7 +40,6 @@ func (ic *imageContexts) add(name string) (*imageMount, error) {
41 41
 		}
42 42
 		ic.byName[name] = im
43 43
 	}
44
-	ic.currentName = name
45 44
 	ic.list = append(ic.list, im)
46 45
 	return im, nil
47 46
 }
... ...
@@ -96,13 +94,6 @@ func (ic *imageContexts) unmount() (retErr error) {
96 96
 	return
97 97
 }
98 98
 
99
-func (ic *imageContexts) isCurrentTarget(target string) bool {
100
-	if target == "" {
101
-		return false
102
-	}
103
-	return strings.EqualFold(ic.currentName, target)
104
-}
105
-
106 99
 func (ic *imageContexts) getCache(id, path string) (interface{}, bool) {
107 100
 	if ic.cache != nil {
108 101
 		if id == "" {
... ...
@@ -35,16 +35,16 @@ import (
35 35
 	"github.com/pkg/errors"
36 36
 )
37 37
 
38
-func (b *Builder) commit(comment string) error {
38
+func (b *Builder) commit(dispatchState *dispatchState, comment string) error {
39 39
 	if b.disableCommit {
40 40
 		return nil
41 41
 	}
42
-	if !b.hasFromImage() {
42
+	if !dispatchState.hasFromImage() {
43 43
 		return errors.New("Please provide a source image with `from` prior to commit")
44 44
 	}
45 45
 
46
-	runConfigWithCommentCmd := copyRunConfig(b.runConfig, withCmdComment(comment))
47
-	hit, err := b.probeCache(b.image, runConfigWithCommentCmd)
46
+	runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment))
47
+	hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd)
48 48
 	if err != nil || hit {
49 49
 		return err
50 50
 	}
... ...
@@ -53,20 +53,21 @@ func (b *Builder) commit(comment string) error {
53 53
 		return err
54 54
 	}
55 55
 
56
-	return b.commitContainer(id, runConfigWithCommentCmd)
56
+	return b.commitContainer(dispatchState, id, runConfigWithCommentCmd)
57 57
 }
58 58
 
59
-func (b *Builder) commitContainer(id string, containerConfig *container.Config) error {
59
+// TODO: see if any args can be dropped
60
+func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error {
60 61
 	if b.disableCommit {
61 62
 		return nil
62 63
 	}
63 64
 
64 65
 	commitCfg := &backend.ContainerCommitConfig{
65 66
 		ContainerCommitConfig: types.ContainerCommitConfig{
66
-			Author: b.maintainer,
67
+			Author: dispatchState.maintainer,
67 68
 			Pause:  true,
68 69
 			// TODO: this should be done by Commit()
69
-			Config: copyRunConfig(b.runConfig),
70
+			Config: copyRunConfig(dispatchState.runConfig),
70 71
 		},
71 72
 		ContainerConfig: containerConfig,
72 73
 	}
... ...
@@ -77,10 +78,8 @@ func (b *Builder) commitContainer(id string, containerConfig *container.Config)
77 77
 		return err
78 78
 	}
79 79
 
80
-	// TODO: this function should return imageID and runConfig instead of setting
81
-	// then on the builder
82
-	b.image = imageID
83
-	b.imageContexts.update(imageID, b.runConfig)
80
+	dispatchState.imageID = imageID
81
+	b.imageContexts.update(imageID, dispatchState.runConfig)
84 82
 	return nil
85 83
 }
86 84
 
... ...
@@ -91,7 +90,9 @@ type copyInfo struct {
91 91
 	decompress bool
92 92
 }
93 93
 
94
-func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalDecompression bool, cmdName string, imageSource *imageMount) error {
94
+// TODO: this needs to be split so that a Builder method doesn't accept req
95
+func (b *Builder) runContextCommand(req dispatchRequest, allowRemote bool, allowLocalDecompression bool, cmdName string, imageSource *imageMount) error {
96
+	args := req.args
95 97
 	if len(args) < 2 {
96 98
 		return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName)
97 99
 	}
... ...
@@ -163,9 +164,9 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
163 163
 
164 164
 	// TODO: should this have been using origPaths instead of srcHash in the comment?
165 165
 	runConfigWithCommentCmd := copyRunConfig(
166
-		b.runConfig,
166
+		req.state.runConfig,
167 167
 		withCmdCommentString(fmt.Sprintf("%s %s in %s ", cmdName, srcHash, dest)))
168
-	if hit, err := b.probeCache(b.image, runConfigWithCommentCmd); err != nil || hit {
168
+	if hit, err := b.probeCache(req.state, runConfigWithCommentCmd); err != nil || hit {
169 169
 		return err
170 170
 	}
171 171
 
... ...
@@ -181,7 +182,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
181 181
 
182 182
 	// Twiddle the destination when it's a relative path - meaning, make it
183 183
 	// relative to the WORKINGDIR
184
-	if dest, err = normaliseDest(cmdName, b.runConfig.WorkingDir, dest); err != nil {
184
+	if dest, err = normaliseDest(cmdName, req.state.runConfig.WorkingDir, dest); err != nil {
185 185
 		return err
186 186
 	}
187 187
 
... ...
@@ -191,7 +192,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
191 191
 		}
192 192
 	}
193 193
 
194
-	return b.commitContainer(container.ID, runConfigWithCommentCmd)
194
+	return b.commitContainer(req.state, container.ID, runConfigWithCommentCmd)
195 195
 }
196 196
 
197 197
 type runConfigModifier func(*container.Config)
... ...
@@ -479,20 +480,20 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
479 479
 	return copyInfos, nil
480 480
 }
481 481
 
482
-func (b *Builder) processImageFrom(img builder.Image) error {
482
+func (b *Builder) processImageFrom(dispatchState *dispatchState, img builder.Image) error {
483 483
 	if img != nil {
484
-		b.image = img.ImageID()
484
+		dispatchState.imageID = img.ImageID()
485 485
 
486 486
 		if img.RunConfig() != nil {
487
-			b.runConfig = img.RunConfig()
487
+			dispatchState.runConfig = img.RunConfig()
488 488
 		}
489 489
 	}
490 490
 
491 491
 	// Check to see if we have a default PATH, note that windows won't
492 492
 	// have one as it's set by HCS
493 493
 	if system.DefaultPathEnv != "" {
494
-		if _, ok := b.runConfigEnvMapping()["PATH"]; !ok {
495
-			b.runConfig.Env = append(b.runConfig.Env,
494
+		if _, ok := dispatchState.runConfigEnvMapping()["PATH"]; !ok {
495
+			dispatchState.runConfig.Env = append(dispatchState.runConfig.Env,
496 496
 				"PATH="+system.DefaultPathEnv)
497 497
 		}
498 498
 	}
... ...
@@ -503,7 +504,7 @@ func (b *Builder) processImageFrom(img builder.Image) error {
503 503
 	}
504 504
 
505 505
 	// Process ONBUILD triggers if they exist
506
-	if nTriggers := len(b.runConfig.OnBuild); nTriggers != 0 {
506
+	if nTriggers := len(dispatchState.runConfig.OnBuild); nTriggers != 0 {
507 507
 		word := "trigger"
508 508
 		if nTriggers > 1 {
509 509
 			word = "triggers"
... ...
@@ -512,21 +513,21 @@ func (b *Builder) processImageFrom(img builder.Image) error {
512 512
 	}
513 513
 
514 514
 	// Copy the ONBUILD triggers, and remove them from the config, since the config will be committed.
515
-	onBuildTriggers := b.runConfig.OnBuild
516
-	b.runConfig.OnBuild = []string{}
515
+	onBuildTriggers := dispatchState.runConfig.OnBuild
516
+	dispatchState.runConfig.OnBuild = []string{}
517 517
 
518 518
 	// Reset stdin settings as all build actions run without stdin
519
-	b.runConfig.OpenStdin = false
520
-	b.runConfig.StdinOnce = false
519
+	dispatchState.runConfig.OpenStdin = false
520
+	dispatchState.runConfig.StdinOnce = false
521 521
 
522 522
 	// parse the ONBUILD triggers by invoking the parser
523 523
 	for _, step := range onBuildTriggers {
524
-		result, err := parser.Parse(strings.NewReader(step))
524
+		dockerfile, err := parser.Parse(strings.NewReader(step))
525 525
 		if err != nil {
526 526
 			return err
527 527
 		}
528 528
 
529
-		for _, n := range result.AST.Children {
529
+		for _, n := range dockerfile.AST.Children {
530 530
 			if err := checkDispatch(n); err != nil {
531 531
 				return err
532 532
 			}
... ...
@@ -540,7 +541,7 @@ func (b *Builder) processImageFrom(img builder.Image) error {
540 540
 			}
541 541
 		}
542 542
 
543
-		if err := dispatchFromDockerfile(b, result); err != nil {
543
+		if _, err := dispatchFromDockerfile(b, dockerfile, dispatchState); err != nil {
544 544
 			return err
545 545
 		}
546 546
 	}
... ...
@@ -551,12 +552,12 @@ func (b *Builder) processImageFrom(img builder.Image) error {
551 551
 // If an image is found, probeCache returns `(true, nil)`.
552 552
 // If no image is found, it returns `(false, nil)`.
553 553
 // If there is any error, it returns `(false, err)`.
554
-func (b *Builder) probeCache(parentID string, runConfig *container.Config) (bool, error) {
554
+func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
555 555
 	c := b.imageCache
556 556
 	if c == nil || b.options.NoCache || b.cacheBusted {
557 557
 		return false, nil
558 558
 	}
559
-	cache, err := c.GetCache(parentID, runConfig)
559
+	cache, err := c.GetCache(dispatchState.imageID, runConfig)
560 560
 	if err != nil {
561 561
 		return false, err
562 562
 	}
... ...
@@ -568,16 +569,13 @@ func (b *Builder) probeCache(parentID string, runConfig *container.Config) (bool
568 568
 
569 569
 	fmt.Fprint(b.Stdout, " ---> Using cache\n")
570 570
 	logrus.Debugf("[BUILDER] Use cached version: %s", runConfig.Cmd)
571
-	b.image = string(cache)
572
-	b.imageContexts.update(b.image, runConfig)
571
+	dispatchState.imageID = string(cache)
572
+	b.imageContexts.update(dispatchState.imageID, runConfig)
573 573
 
574 574
 	return true, nil
575 575
 }
576 576
 
577 577
 func (b *Builder) create(runConfig *container.Config) (string, error) {
578
-	if !b.hasFromImage() {
579
-		return "", errors.New("Please provide a source image with `from` prior to run")
580
-	}
581 578
 	resources := container.Resources{
582 579
 		CgroupParent: b.options.CgroupParent,
583 580
 		CPUShares:    b.options.CPUShares,