Browse code

Introduce a typed command system and 2 phase parse/dispatch build

This is a work base to introduce more features like build time
dockerfile optimisations, dependency analysis and parallel build, as
well as a first step to go from a dispatch-inline process to a
frontend+backend process.

Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>

Simon Ferquel authored on 2017/05/23 00:21:17
Showing 27 changed files
1 1
deleted file mode 100644
... ...
@@ -1,183 +0,0 @@
1
-package dockerfile
2
-
3
-import (
4
-	"fmt"
5
-	"strings"
6
-)
7
-
8
-// FlagType is the type of the build flag
9
-type FlagType int
10
-
11
-const (
12
-	boolType FlagType = iota
13
-	stringType
14
-)
15
-
16
-// BFlags contains all flags information for the builder
17
-type BFlags struct {
18
-	Args  []string // actual flags/args from cmd line
19
-	flags map[string]*Flag
20
-	used  map[string]*Flag
21
-	Err   error
22
-}
23
-
24
-// Flag contains all information for a flag
25
-type Flag struct {
26
-	bf       *BFlags
27
-	name     string
28
-	flagType FlagType
29
-	Value    string
30
-}
31
-
32
-// NewBFlags returns the new BFlags struct
33
-func NewBFlags() *BFlags {
34
-	return &BFlags{
35
-		flags: make(map[string]*Flag),
36
-		used:  make(map[string]*Flag),
37
-	}
38
-}
39
-
40
-// NewBFlagsWithArgs returns the new BFlags struct with Args set to args
41
-func NewBFlagsWithArgs(args []string) *BFlags {
42
-	flags := NewBFlags()
43
-	flags.Args = args
44
-	return flags
45
-}
46
-
47
-// AddBool adds a bool flag to BFlags
48
-// Note, any error will be generated when Parse() is called (see Parse).
49
-func (bf *BFlags) AddBool(name string, def bool) *Flag {
50
-	flag := bf.addFlag(name, boolType)
51
-	if flag == nil {
52
-		return nil
53
-	}
54
-	if def {
55
-		flag.Value = "true"
56
-	} else {
57
-		flag.Value = "false"
58
-	}
59
-	return flag
60
-}
61
-
62
-// AddString adds a string flag to BFlags
63
-// Note, any error will be generated when Parse() is called (see Parse).
64
-func (bf *BFlags) AddString(name string, def string) *Flag {
65
-	flag := bf.addFlag(name, stringType)
66
-	if flag == nil {
67
-		return nil
68
-	}
69
-	flag.Value = def
70
-	return flag
71
-}
72
-
73
-// addFlag is a generic func used by the other AddXXX() func
74
-// to add a new flag to the BFlags struct.
75
-// Note, any error will be generated when Parse() is called (see Parse).
76
-func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag {
77
-	if _, ok := bf.flags[name]; ok {
78
-		bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
79
-		return nil
80
-	}
81
-
82
-	newFlag := &Flag{
83
-		bf:       bf,
84
-		name:     name,
85
-		flagType: flagType,
86
-	}
87
-	bf.flags[name] = newFlag
88
-
89
-	return newFlag
90
-}
91
-
92
-// IsUsed checks if the flag is used
93
-func (fl *Flag) IsUsed() bool {
94
-	if _, ok := fl.bf.used[fl.name]; ok {
95
-		return true
96
-	}
97
-	return false
98
-}
99
-
100
-// IsTrue checks if a bool flag is true
101
-func (fl *Flag) IsTrue() bool {
102
-	if fl.flagType != boolType {
103
-		// Should never get here
104
-		panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
105
-	}
106
-	return fl.Value == "true"
107
-}
108
-
109
-// Parse parses and checks if the BFlags is valid.
110
-// Any error noticed during the AddXXX() funcs will be generated/returned
111
-// here.  We do this because an error during AddXXX() is more like a
112
-// compile time error so it doesn't matter too much when we stop our
113
-// processing as long as we do stop it, so this allows the code
114
-// around AddXXX() to be just:
115
-//     defFlag := AddString("description", "")
116
-// w/o needing to add an if-statement around each one.
117
-func (bf *BFlags) Parse() error {
118
-	// If there was an error while defining the possible flags
119
-	// go ahead and bubble it back up here since we didn't do it
120
-	// earlier in the processing
121
-	if bf.Err != nil {
122
-		return fmt.Errorf("Error setting up flags: %s", bf.Err)
123
-	}
124
-
125
-	for _, arg := range bf.Args {
126
-		if !strings.HasPrefix(arg, "--") {
127
-			return fmt.Errorf("Arg should start with -- : %s", arg)
128
-		}
129
-
130
-		if arg == "--" {
131
-			return nil
132
-		}
133
-
134
-		arg = arg[2:]
135
-		value := ""
136
-
137
-		index := strings.Index(arg, "=")
138
-		if index >= 0 {
139
-			value = arg[index+1:]
140
-			arg = arg[:index]
141
-		}
142
-
143
-		flag, ok := bf.flags[arg]
144
-		if !ok {
145
-			return fmt.Errorf("Unknown flag: %s", arg)
146
-		}
147
-
148
-		if _, ok = bf.used[arg]; ok {
149
-			return fmt.Errorf("Duplicate flag specified: %s", arg)
150
-		}
151
-
152
-		bf.used[arg] = flag
153
-
154
-		switch flag.flagType {
155
-		case boolType:
156
-			// value == "" is only ok if no "=" was specified
157
-			if index >= 0 && value == "" {
158
-				return fmt.Errorf("Missing a value on flag: %s", arg)
159
-			}
160
-
161
-			lower := strings.ToLower(value)
162
-			if lower == "" {
163
-				flag.Value = "true"
164
-			} else if lower == "true" || lower == "false" {
165
-				flag.Value = lower
166
-			} else {
167
-				return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
168
-			}
169
-
170
-		case stringType:
171
-			if index < 0 {
172
-				return fmt.Errorf("Missing a value on flag: %s", arg)
173
-			}
174
-			flag.Value = value
175
-
176
-		default:
177
-			panic("No idea what kind of flag we have! Should never get here!")
178
-		}
179
-
180
-	}
181
-
182
-	return nil
183
-}
184 1
deleted file mode 100644
... ...
@@ -1,187 +0,0 @@
1
-package dockerfile
2
-
3
-import (
4
-	"testing"
5
-)
6
-
7
-func TestBuilderFlags(t *testing.T) {
8
-	var expected string
9
-	var err error
10
-
11
-	// ---
12
-
13
-	bf := NewBFlags()
14
-	bf.Args = []string{}
15
-	if err := bf.Parse(); err != nil {
16
-		t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err)
17
-	}
18
-
19
-	// ---
20
-
21
-	bf = NewBFlags()
22
-	bf.Args = []string{"--"}
23
-	if err := bf.Parse(); err != nil {
24
-		t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err)
25
-	}
26
-
27
-	// ---
28
-
29
-	bf = NewBFlags()
30
-	flStr1 := bf.AddString("str1", "")
31
-	flBool1 := bf.AddBool("bool1", false)
32
-	bf.Args = []string{}
33
-	if err = bf.Parse(); err != nil {
34
-		t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err)
35
-	}
36
-
37
-	if flStr1.IsUsed() {
38
-		t.Fatal("Test3 - str1 was not used!")
39
-	}
40
-	if flBool1.IsUsed() {
41
-		t.Fatal("Test3 - bool1 was not used!")
42
-	}
43
-
44
-	// ---
45
-
46
-	bf = NewBFlags()
47
-	flStr1 = bf.AddString("str1", "HI")
48
-	flBool1 = bf.AddBool("bool1", false)
49
-	bf.Args = []string{}
50
-
51
-	if err = bf.Parse(); err != nil {
52
-		t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err)
53
-	}
54
-
55
-	if flStr1.Value != "HI" {
56
-		t.Fatal("Str1 was supposed to default to: HI")
57
-	}
58
-	if flBool1.IsTrue() {
59
-		t.Fatal("Bool1 was supposed to default to: false")
60
-	}
61
-	if flStr1.IsUsed() {
62
-		t.Fatal("Str1 was not used!")
63
-	}
64
-	if flBool1.IsUsed() {
65
-		t.Fatal("Bool1 was not used!")
66
-	}
67
-
68
-	// ---
69
-
70
-	bf = NewBFlags()
71
-	flStr1 = bf.AddString("str1", "HI")
72
-	bf.Args = []string{"--str1"}
73
-
74
-	if err = bf.Parse(); err == nil {
75
-		t.Fatalf("Test %q was supposed to fail", bf.Args)
76
-	}
77
-
78
-	// ---
79
-
80
-	bf = NewBFlags()
81
-	flStr1 = bf.AddString("str1", "HI")
82
-	bf.Args = []string{"--str1="}
83
-
84
-	if err = bf.Parse(); err != nil {
85
-		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
86
-	}
87
-
88
-	expected = ""
89
-	if flStr1.Value != expected {
90
-		t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
91
-	}
92
-
93
-	// ---
94
-
95
-	bf = NewBFlags()
96
-	flStr1 = bf.AddString("str1", "HI")
97
-	bf.Args = []string{"--str1=BYE"}
98
-
99
-	if err = bf.Parse(); err != nil {
100
-		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
101
-	}
102
-
103
-	expected = "BYE"
104
-	if flStr1.Value != expected {
105
-		t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
106
-	}
107
-
108
-	// ---
109
-
110
-	bf = NewBFlags()
111
-	flBool1 = bf.AddBool("bool1", false)
112
-	bf.Args = []string{"--bool1"}
113
-
114
-	if err = bf.Parse(); err != nil {
115
-		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
116
-	}
117
-
118
-	if !flBool1.IsTrue() {
119
-		t.Fatal("Test-b1 Bool1 was supposed to be true")
120
-	}
121
-
122
-	// ---
123
-
124
-	bf = NewBFlags()
125
-	flBool1 = bf.AddBool("bool1", false)
126
-	bf.Args = []string{"--bool1=true"}
127
-
128
-	if err = bf.Parse(); err != nil {
129
-		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
130
-	}
131
-
132
-	if !flBool1.IsTrue() {
133
-		t.Fatal("Test-b2 Bool1 was supposed to be true")
134
-	}
135
-
136
-	// ---
137
-
138
-	bf = NewBFlags()
139
-	flBool1 = bf.AddBool("bool1", false)
140
-	bf.Args = []string{"--bool1=false"}
141
-
142
-	if err = bf.Parse(); err != nil {
143
-		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
144
-	}
145
-
146
-	if flBool1.IsTrue() {
147
-		t.Fatal("Test-b3 Bool1 was supposed to be false")
148
-	}
149
-
150
-	// ---
151
-
152
-	bf = NewBFlags()
153
-	flBool1 = bf.AddBool("bool1", false)
154
-	bf.Args = []string{"--bool1=false1"}
155
-
156
-	if err = bf.Parse(); err == nil {
157
-		t.Fatalf("Test %q was supposed to fail", bf.Args)
158
-	}
159
-
160
-	// ---
161
-
162
-	bf = NewBFlags()
163
-	flBool1 = bf.AddBool("bool1", false)
164
-	bf.Args = []string{"--bool2"}
165
-
166
-	if err = bf.Parse(); err == nil {
167
-		t.Fatalf("Test %q was supposed to fail", bf.Args)
168
-	}
169
-
170
-	// ---
171
-
172
-	bf = NewBFlags()
173
-	flStr1 = bf.AddString("str1", "HI")
174
-	flBool1 = bf.AddBool("bool1", false)
175
-	bf.Args = []string{"--bool1", "--str1=BYE"}
176
-
177
-	if err = bf.Parse(); err != nil {
178
-		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
179
-	}
180
-
181
-	if flStr1.Value != "BYE" {
182
-		t.Fatalf("Test %s, str1 should be BYE", bf.Args)
183
-	}
184
-	if !flBool1.IsTrue() {
185
-		t.Fatalf("Test %s, bool1 should be true", bf.Args)
186
-	}
187
-}
... ...
@@ -42,6 +42,26 @@ func newBuildArgs(argsFromOptions map[string]*string) *buildArgs {
42 42
 	}
43 43
 }
44 44
 
45
+func (b *buildArgs) Clone() *buildArgs {
46
+	result := newBuildArgs(b.argsFromOptions)
47
+	for k, v := range b.allowedBuildArgs {
48
+		result.allowedBuildArgs[k] = v
49
+	}
50
+	for k, v := range b.allowedMetaArgs {
51
+		result.allowedMetaArgs[k] = v
52
+	}
53
+	for k := range b.referencedArgs {
54
+		result.referencedArgs[k] = struct{}{}
55
+	}
56
+	return result
57
+}
58
+
59
+func (b *buildArgs) MergeReferencedArgs(other *buildArgs) {
60
+	for k := range other.referencedArgs {
61
+		b.referencedArgs[k] = struct{}{}
62
+	}
63
+}
64
+
45 65
 // WarnOnUnusedBuildArgs checks if there are any leftover build-args that were
46 66
 // passed but not consumed during build. Print a warning, if there are any.
47 67
 func (b *buildArgs) WarnOnUnusedBuildArgs(out io.Writer) {
... ...
@@ -13,7 +13,7 @@ import (
13 13
 	"github.com/docker/docker/api/types/backend"
14 14
 	"github.com/docker/docker/api/types/container"
15 15
 	"github.com/docker/docker/builder"
16
-	"github.com/docker/docker/builder/dockerfile/command"
16
+	"github.com/docker/docker/builder/dockerfile/instructions"
17 17
 	"github.com/docker/docker/builder/dockerfile/parser"
18 18
 	"github.com/docker/docker/builder/fscache"
19 19
 	"github.com/docker/docker/builder/remotecontext"
... ...
@@ -41,6 +41,10 @@ var validCommitCommands = map[string]bool{
41 41
 	"workdir":     true,
42 42
 }
43 43
 
44
+const (
45
+	stepFormat = "Step %d/%d : %v"
46
+)
47
+
44 48
 // SessionGetter is object used to get access to a session by uuid
45 49
 type SessionGetter interface {
46 50
 	Get(ctx context.Context, uuid string) (session.Caller, error)
... ...
@@ -176,9 +180,7 @@ type Builder struct {
176 176
 	clientCtx context.Context
177 177
 
178 178
 	idMappings       *idtools.IDMappings
179
-	buildStages      *buildStages
180 179
 	disableCommit    bool
181
-	buildArgs        *buildArgs
182 180
 	imageSources     *imageSources
183 181
 	pathCache        pathCache
184 182
 	containerManager *containerManager
... ...
@@ -218,8 +220,6 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
218 218
 		Output:           options.ProgressWriter.Output,
219 219
 		docker:           options.Backend,
220 220
 		idMappings:       options.IDMappings,
221
-		buildArgs:        newBuildArgs(config.BuildArgs),
222
-		buildStages:      newBuildStages(),
223 221
 		imageSources:     newImageSources(clientCtx, options),
224 222
 		pathCache:        options.PathCache,
225 223
 		imageProber:      newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache),
... ...
@@ -237,24 +237,27 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil
237 237
 
238 238
 	addNodesForLabelOption(dockerfile.AST, b.options.Labels)
239 239
 
240
-	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
241
-		buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
240
+	stages, metaArgs, err := instructions.Parse(dockerfile.AST)
241
+	if err != nil {
242
+		if instructions.IsUnknownInstruction(err) {
243
+			buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
244
+		}
242 245
 		return nil, validationError{err}
243 246
 	}
247
+	if b.options.Target != "" {
248
+		targetIx, found := instructions.HasStage(stages, b.options.Target)
249
+		if !found {
250
+			buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
251
+			return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
252
+		}
253
+		stages = stages[:targetIx+1]
254
+	}
244 255
 
245
-	dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
256
+	dockerfile.PrintWarnings(b.Stderr)
257
+	dispatchState, err := b.dispatchDockerfileWithCancellation(stages, metaArgs, dockerfile.EscapeToken, source)
246 258
 	if err != nil {
247 259
 		return nil, err
248 260
 	}
249
-
250
-	if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
251
-		buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
252
-		return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
253
-	}
254
-
255
-	dockerfile.PrintWarnings(b.Stderr)
256
-	b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr)
257
-
258 261
 	if dispatchState.imageID == "" {
259 262
 		buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
260 263
 		return nil, errors.New("No image was generated. Is your Dockerfile empty?")
... ...
@@ -269,61 +272,91 @@ func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error
269 269
 	return aux.Emit(types.BuildResult{ID: state.imageID})
270 270
 }
271 271
 
272
-func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
273
-	shlex := NewShellLex(dockerfile.EscapeToken)
274
-	state := newDispatchState()
275
-	total := len(dockerfile.AST.Children)
276
-	var err error
277
-	for i, n := range dockerfile.AST.Children {
278
-		select {
279
-		case <-b.clientCtx.Done():
280
-			logrus.Debug("Builder: build cancelled!")
281
-			fmt.Fprint(b.Stdout, "Build cancelled")
282
-			buildsFailed.WithValues(metricsBuildCanceled).Inc()
283
-			return nil, errors.New("Build cancelled")
284
-		default:
285
-			// Not cancelled yet, keep going...
286
-		}
272
+func processMetaArg(meta instructions.ArgCommand, shlex *ShellLex, args *buildArgs) error {
273
+	// ShellLex currently only support the concatenated string format
274
+	envs := convertMapToEnvList(args.GetAllAllowed())
275
+	if err := meta.Expand(func(word string) (string, error) {
276
+		return shlex.ProcessWord(word, envs)
277
+	}); err != nil {
278
+		return err
279
+	}
280
+	args.AddArg(meta.Key, meta.Value)
281
+	args.AddMetaArg(meta.Key, meta.Value)
282
+	return nil
283
+}
287 284
 
288
-		// If this is a FROM and we have a previous image then
289
-		// emit an aux message for that image since it is the
290
-		// end of the previous stage
291
-		if n.Value == command.From {
292
-			if err := emitImageID(b.Aux, state); err != nil {
293
-				return nil, err
294
-			}
285
+func printCommand(out io.Writer, currentCommandIndex int, totalCommands int, cmd interface{}) int {
286
+	fmt.Fprintf(out, stepFormat, currentCommandIndex, totalCommands, cmd)
287
+	fmt.Fprintln(out)
288
+	return currentCommandIndex + 1
289
+}
290
+
291
+func (b *Builder) dispatchDockerfileWithCancellation(parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) {
292
+	dispatchRequest := dispatchRequest{}
293
+	buildArgs := newBuildArgs(b.options.BuildArgs)
294
+	totalCommands := len(metaArgs) + len(parseResult)
295
+	currentCommandIndex := 1
296
+	for _, stage := range parseResult {
297
+		totalCommands += len(stage.Commands)
298
+	}
299
+	shlex := NewShellLex(escapeToken)
300
+	for _, meta := range metaArgs {
301
+		currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &meta)
302
+
303
+		err := processMetaArg(meta, shlex, buildArgs)
304
+		if err != nil {
305
+			return nil, err
295 306
 		}
307
+	}
308
+
309
+	stagesResults := newStagesBuildResults()
296 310
 
297
-		if n.Value == command.From && state.isCurrentStage(b.options.Target) {
298
-			break
311
+	for _, stage := range parseResult {
312
+		if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil {
313
+			return nil, err
299 314
 		}
315
+		dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults)
300 316
 
301
-		opts := dispatchOptions{
302
-			state:   state,
303
-			stepMsg: formatStep(i, total),
304
-			node:    n,
305
-			shlex:   shlex,
306
-			source:  source,
317
+		currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode)
318
+		if err := initializeStage(dispatchRequest, &stage); err != nil {
319
+			return nil, err
307 320
 		}
308
-		if state, err = b.dispatch(opts); err != nil {
309
-			if b.options.ForceRemove {
310
-				b.containerManager.RemoveAll(b.Stdout)
321
+		dispatchRequest.state.updateRunConfig()
322
+		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
323
+		for _, cmd := range stage.Commands {
324
+			select {
325
+			case <-b.clientCtx.Done():
326
+				logrus.Debug("Builder: build cancelled!")
327
+				fmt.Fprint(b.Stdout, "Build cancelled\n")
328
+				buildsFailed.WithValues(metricsBuildCanceled).Inc()
329
+				return nil, errors.New("Build cancelled")
330
+			default:
331
+				// Not cancelled yet, keep going...
311 332
 			}
333
+
334
+			currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd)
335
+
336
+			if err := dispatch(dispatchRequest, cmd); err != nil {
337
+				return nil, err
338
+			}
339
+
340
+			dispatchRequest.state.updateRunConfig()
341
+			fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
342
+
343
+		}
344
+		if err := emitImageID(b.Aux, dispatchRequest.state); err != nil {
312 345
 			return nil, err
313 346
 		}
314
-
315
-		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
316
-		if b.options.Remove {
317
-			b.containerManager.RemoveAll(b.Stdout)
347
+		buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs)
348
+		if err := commitStage(dispatchRequest.state, stagesResults); err != nil {
349
+			return nil, err
318 350
 		}
319 351
 	}
320
-
321
-	// Emit a final aux message for the final image
322
-	if err := emitImageID(b.Aux, state); err != nil {
323
-		return nil, err
352
+	if b.options.Remove {
353
+		b.containerManager.RemoveAll(b.Stdout)
324 354
 	}
325
-
326
-	return state, nil
355
+	buildArgs.WarnOnUnusedBuildArgs(b.Stdout)
356
+	return dispatchRequest.state, nil
327 357
 }
328 358
 
329 359
 func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
... ...
@@ -380,39 +413,33 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
380 380
 	b.Stderr = ioutil.Discard
381 381
 	b.disableCommit = true
382 382
 
383
-	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
384
-		return nil, validationError{err}
383
+	commands := []instructions.Command{}
384
+	for _, n := range dockerfile.AST.Children {
385
+		cmd, err := instructions.ParseCommand(n)
386
+		if err != nil {
387
+			return nil, validationError{err}
388
+		}
389
+		commands = append(commands, cmd)
385 390
 	}
386
-	dispatchState := newDispatchState()
387
-	dispatchState.runConfig = config
388
-	return dispatchFromDockerfile(b, dockerfile, dispatchState, nil)
389
-}
390 391
 
391
-func checkDispatchDockerfile(dockerfile *parser.Node) error {
392
-	for _, n := range dockerfile.Children {
393
-		if err := checkDispatch(n); err != nil {
394
-			return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine)
392
+	dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, newBuildArgs(b.options.BuildArgs), newStagesBuildResults())
393
+	dispatchRequest.state.runConfig = config
394
+	dispatchRequest.state.imageID = config.Image
395
+	for _, cmd := range commands {
396
+		err := dispatch(dispatchRequest, cmd)
397
+		if err != nil {
398
+			return nil, validationError{err}
395 399
 		}
400
+		dispatchRequest.state.updateRunConfig()
396 401
 	}
397
-	return nil
402
+
403
+	return dispatchRequest.state.runConfig, nil
398 404
 }
399 405
 
400
-func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
401
-	shlex := NewShellLex(result.EscapeToken)
402
-	ast := result.AST
403
-	total := len(ast.Children)
404
-
405
-	for i, n := range ast.Children {
406
-		opts := dispatchOptions{
407
-			state:   dispatchState,
408
-			stepMsg: formatStep(i, total),
409
-			node:    n,
410
-			shlex:   shlex,
411
-			source:  source,
412
-		}
413
-		if _, err := b.dispatch(opts); err != nil {
414
-			return nil, err
415
-		}
406
+func convertMapToEnvList(m map[string]string) []string {
407
+	result := []string{}
408
+	for k, v := range m {
409
+		result = append(result, k+"="+v)
416 410
 	}
417
-	return dispatchState.runConfig, nil
411
+	return result
418 412
 }
... ...
@@ -10,17 +10,15 @@ package dockerfile
10 10
 import (
11 11
 	"bytes"
12 12
 	"fmt"
13
-	"regexp"
14 13
 	"runtime"
15 14
 	"sort"
16
-	"strconv"
17 15
 	"strings"
18
-	"time"
19 16
 
20 17
 	"github.com/docker/docker/api"
21 18
 	"github.com/docker/docker/api/types/container"
22 19
 	"github.com/docker/docker/api/types/strslice"
23 20
 	"github.com/docker/docker/builder"
21
+	"github.com/docker/docker/builder/dockerfile/instructions"
24 22
 	"github.com/docker/docker/builder/dockerfile/parser"
25 23
 	"github.com/docker/docker/image"
26 24
 	"github.com/docker/docker/pkg/jsonmessage"
... ...
@@ -36,32 +34,14 @@ import (
36 36
 // Sets the environment variable foo to bar, also makes interpolation
37 37
 // in the dockerfile available from the next statement on via ${foo}.
38 38
 //
39
-func env(req dispatchRequest) error {
40
-	if len(req.args) == 0 {
41
-		return errAtLeastOneArgument("ENV")
42
-	}
43
-
44
-	if len(req.args)%2 != 0 {
45
-		// should never get here, but just in case
46
-		return errTooManyArguments("ENV")
47
-	}
48
-
49
-	if err := req.flags.Parse(); err != nil {
50
-		return err
51
-	}
52
-
53
-	runConfig := req.state.runConfig
39
+func dispatchEnv(d dispatchRequest, c *instructions.EnvCommand) error {
40
+	runConfig := d.state.runConfig
54 41
 	commitMessage := bytes.NewBufferString("ENV")
42
+	for _, e := range c.Env {
43
+		name := e.Key
44
+		newVar := e.String()
55 45
 
56
-	for j := 0; j < len(req.args); j += 2 {
57
-		if len(req.args[j]) == 0 {
58
-			return errBlankCommandNames("ENV")
59
-		}
60
-		name := req.args[j]
61
-		value := req.args[j+1]
62
-		newVar := name + "=" + value
63 46
 		commitMessage.WriteString(" " + newVar)
64
-
65 47
 		gotOne := false
66 48
 		for i, envVar := range runConfig.Env {
67 49
 			envParts := strings.SplitN(envVar, "=", 2)
... ...
@@ -76,64 +56,32 @@ func env(req dispatchRequest) error {
76 76
 			runConfig.Env = append(runConfig.Env, newVar)
77 77
 		}
78 78
 	}
79
-
80
-	return req.builder.commit(req.state, commitMessage.String())
79
+	return d.builder.commit(d.state, commitMessage.String())
81 80
 }
82 81
 
83 82
 // MAINTAINER some text <maybe@an.email.address>
84 83
 //
85 84
 // Sets the maintainer metadata.
86
-func maintainer(req dispatchRequest) error {
87
-	if len(req.args) != 1 {
88
-		return errExactlyOneArgument("MAINTAINER")
89
-	}
85
+func dispatchMaintainer(d dispatchRequest, c *instructions.MaintainerCommand) error {
90 86
 
91
-	if err := req.flags.Parse(); err != nil {
92
-		return err
93
-	}
94
-
95
-	maintainer := req.args[0]
96
-	req.state.maintainer = maintainer
97
-	return req.builder.commit(req.state, "MAINTAINER "+maintainer)
87
+	d.state.maintainer = c.Maintainer
88
+	return d.builder.commit(d.state, "MAINTAINER "+c.Maintainer)
98 89
 }
99 90
 
100 91
 // LABEL some json data describing the image
101 92
 //
102 93
 // Sets the Label variable foo to bar,
103 94
 //
104
-func label(req dispatchRequest) error {
105
-	if len(req.args) == 0 {
106
-		return errAtLeastOneArgument("LABEL")
107
-	}
108
-	if len(req.args)%2 != 0 {
109
-		// should never get here, but just in case
110
-		return errTooManyArguments("LABEL")
111
-	}
112
-
113
-	if err := req.flags.Parse(); err != nil {
114
-		return err
95
+func dispatchLabel(d dispatchRequest, c *instructions.LabelCommand) error {
96
+	if d.state.runConfig.Labels == nil {
97
+		d.state.runConfig.Labels = make(map[string]string)
115 98
 	}
116
-
117 99
 	commitStr := "LABEL"
118
-	runConfig := req.state.runConfig
119
-
120
-	if runConfig.Labels == nil {
121
-		runConfig.Labels = map[string]string{}
122
-	}
123
-
124
-	for j := 0; j < len(req.args); j++ {
125
-		name := req.args[j]
126
-		if name == "" {
127
-			return errBlankCommandNames("LABEL")
128
-		}
129
-
130
-		value := req.args[j+1]
131
-		commitStr += " " + name + "=" + value
132
-
133
-		runConfig.Labels[name] = value
134
-		j++
100
+	for _, v := range c.Labels {
101
+		d.state.runConfig.Labels[v.Key] = v.Value
102
+		commitStr += " " + v.String()
135 103
 	}
136
-	return req.builder.commit(req.state, commitStr)
104
+	return d.builder.commit(d.state, commitStr)
137 105
 }
138 106
 
139 107
 // ADD foo /path
... ...
@@ -141,257 +89,172 @@ func label(req dispatchRequest) error {
141 141
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
142 142
 // exist here. If you do not wish to have this automatic handling, use COPY.
143 143
 //
144
-func add(req dispatchRequest) error {
145
-	if len(req.args) < 2 {
146
-		return errAtLeastTwoArguments("ADD")
147
-	}
148
-
149
-	flChown := req.flags.AddString("chown", "")
150
-	if err := req.flags.Parse(); err != nil {
151
-		return err
152
-	}
153
-
154
-	downloader := newRemoteSourceDownloader(req.builder.Output, req.builder.Stdout)
155
-	copier := copierFromDispatchRequest(req, downloader, nil)
144
+func dispatchAdd(d dispatchRequest, c *instructions.AddCommand) error {
145
+	downloader := newRemoteSourceDownloader(d.builder.Output, d.builder.Stdout)
146
+	copier := copierFromDispatchRequest(d, downloader, nil)
156 147
 	defer copier.Cleanup()
157
-	copyInstruction, err := copier.createCopyInstruction(req.args, "ADD")
148
+
149
+	copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "ADD")
158 150
 	if err != nil {
159 151
 		return err
160 152
 	}
161
-	copyInstruction.chownStr = flChown.Value
153
+	copyInstruction.chownStr = c.Chown
162 154
 	copyInstruction.allowLocalDecompression = true
163 155
 
164
-	return req.builder.performCopy(req.state, copyInstruction)
156
+	return d.builder.performCopy(d.state, copyInstruction)
165 157
 }
166 158
 
167 159
 // COPY foo /path
168 160
 //
169 161
 // Same as 'ADD' but without the tar and remote url handling.
170 162
 //
171
-func dispatchCopy(req dispatchRequest) error {
172
-	if len(req.args) < 2 {
173
-		return errAtLeastTwoArguments("COPY")
174
-	}
175
-
176
-	flFrom := req.flags.AddString("from", "")
177
-	flChown := req.flags.AddString("chown", "")
178
-	if err := req.flags.Parse(); err != nil {
179
-		return err
180
-	}
181
-
182
-	im, err := req.builder.getImageMount(flFrom)
183
-	if err != nil {
184
-		return errors.Wrapf(err, "invalid from flag value %s", flFrom.Value)
163
+func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error {
164
+	var im *imageMount
165
+	var err error
166
+	if c.From != "" {
167
+		im, err = d.getImageMount(c.From)
168
+		if err != nil {
169
+			return errors.Wrapf(err, "invalid from flag value %s", c.From)
170
+		}
185 171
 	}
186
-
187
-	copier := copierFromDispatchRequest(req, errOnSourceDownload, im)
172
+	copier := copierFromDispatchRequest(d, errOnSourceDownload, im)
188 173
 	defer copier.Cleanup()
189
-	copyInstruction, err := copier.createCopyInstruction(req.args, "COPY")
174
+	copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "COPY")
190 175
 	if err != nil {
191 176
 		return err
192 177
 	}
193
-	copyInstruction.chownStr = flChown.Value
178
+	copyInstruction.chownStr = c.Chown
194 179
 
195
-	return req.builder.performCopy(req.state, copyInstruction)
180
+	return d.builder.performCopy(d.state, copyInstruction)
196 181
 }
197 182
 
198
-func (b *Builder) getImageMount(fromFlag *Flag) (*imageMount, error) {
199
-	if !fromFlag.IsUsed() {
183
+func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error) {
184
+	if imageRefOrID == "" {
200 185
 		// TODO: this could return the source in the default case as well?
201 186
 		return nil, nil
202 187
 	}
203 188
 
204 189
 	var localOnly bool
205
-	imageRefOrID := fromFlag.Value
206
-	stage, err := b.buildStages.get(fromFlag.Value)
190
+	stage, err := d.stages.get(imageRefOrID)
207 191
 	if err != nil {
208 192
 		return nil, err
209 193
 	}
210 194
 	if stage != nil {
211
-		imageRefOrID = stage.ImageID()
195
+		imageRefOrID = stage.Image
212 196
 		localOnly = true
213 197
 	}
214
-	return b.imageSources.Get(imageRefOrID, localOnly)
198
+	return d.builder.imageSources.Get(imageRefOrID, localOnly)
215 199
 }
216 200
 
217 201
 // FROM imagename[:tag | @digest] [AS build-stage-name]
218 202
 //
219
-func from(req dispatchRequest) error {
220
-	stageName, err := parseBuildStageName(req.args)
221
-	if err != nil {
222
-		return err
223
-	}
224
-
225
-	if err := req.flags.Parse(); err != nil {
226
-		return err
227
-	}
228
-
229
-	req.builder.imageProber.Reset()
230
-	image, err := req.builder.getFromImage(req.shlex, req.args[0])
203
+func initializeStage(d dispatchRequest, cmd *instructions.Stage) error {
204
+	d.builder.imageProber.Reset()
205
+	image, err := d.getFromImage(d.shlex, cmd.BaseName)
231 206
 	if err != nil {
232 207
 		return err
233 208
 	}
234
-	if err := req.builder.buildStages.add(stageName, image); err != nil {
235
-		return err
236
-	}
237
-	req.state.beginStage(stageName, image)
238
-	req.builder.buildArgs.ResetAllowed()
239
-	if image.ImageID() == "" {
240
-		// Typically this means they used "FROM scratch"
241
-		return nil
209
+	state := d.state
210
+	state.beginStage(cmd.Name, image)
211
+	if len(state.runConfig.OnBuild) > 0 {
212
+		triggers := state.runConfig.OnBuild
213
+		state.runConfig.OnBuild = nil
214
+		return dispatchTriggeredOnBuild(d, triggers)
242 215
 	}
243
-
244
-	return processOnBuild(req)
216
+	return nil
245 217
 }
246 218
 
247
-func parseBuildStageName(args []string) (string, error) {
248
-	stageName := ""
249
-	switch {
250
-	case len(args) == 3 && strings.EqualFold(args[1], "as"):
251
-		stageName = strings.ToLower(args[2])
252
-		if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok {
253
-			return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName)
219
+func dispatchTriggeredOnBuild(d dispatchRequest, triggers []string) error {
220
+	fmt.Fprintf(d.builder.Stdout, "# Executing %d build trigger", len(triggers))
221
+	if len(triggers) > 1 {
222
+		fmt.Fprint(d.builder.Stdout, "s")
223
+	}
224
+	fmt.Fprintln(d.builder.Stdout)
225
+	for _, trigger := range triggers {
226
+		d.state.updateRunConfig()
227
+		ast, err := parser.Parse(strings.NewReader(trigger))
228
+		if err != nil {
229
+			return err
230
+		}
231
+		if len(ast.AST.Children) != 1 {
232
+			return errors.New("onbuild trigger should be a single expression")
233
+		}
234
+		cmd, err := instructions.ParseCommand(ast.AST.Children[0])
235
+		if err != nil {
236
+			if instructions.IsUnknownInstruction(err) {
237
+				buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
238
+			}
239
+			return err
240
+		}
241
+		err = dispatch(d, cmd)
242
+		if err != nil {
243
+			return err
254 244
 		}
255
-	case len(args) != 1:
256
-		return "", errors.New("FROM requires either one or three arguments")
257 245
 	}
258
-
259
-	return stageName, nil
246
+	return nil
260 247
 }
261 248
 
262
-// scratchImage is used as a token for the empty base image.
249
+// scratchImage is used as a token for the empty base image. It uses buildStage
250
+// as a convenient implementation of builder.Image, but is not actually a
251
+// buildStage.
263 252
 var scratchImage builder.Image = &image.Image{}
264 253
 
265
-func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
254
+func (d *dispatchRequest) getExpandedImageName(shlex *ShellLex, name string) (string, error) {
266 255
 	substitutionArgs := []string{}
267
-	for key, value := range b.buildArgs.GetAllMeta() {
256
+	for key, value := range d.state.buildArgs.GetAllMeta() {
268 257
 		substitutionArgs = append(substitutionArgs, key+"="+value)
269 258
 	}
270 259
 
271 260
 	name, err := shlex.ProcessWord(name, substitutionArgs)
272 261
 	if err != nil {
273
-		return nil, err
262
+		return "", err
274 263
 	}
275
-
264
+	return name, nil
265
+}
266
+func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) {
276 267
 	var localOnly bool
277
-	if stage, ok := b.buildStages.getByName(name); ok {
278
-		name = stage.ImageID()
268
+	if im, ok := d.stages.getByName(name); ok {
269
+		name = im.Image
279 270
 		localOnly = true
280 271
 	}
281 272
 
282 273
 	// Windows cannot support a container with no base image unless it is LCOW.
283 274
 	if name == api.NoBaseImageSpecifier {
284 275
 		if runtime.GOOS == "windows" {
285
-			if b.platform == "windows" || (b.platform != "windows" && !system.LCOWSupported()) {
276
+			if d.builder.platform == "windows" || (d.builder.platform != "windows" && !system.LCOWSupported()) {
286 277
 				return nil, errors.New("Windows does not support FROM scratch")
287 278
 			}
288 279
 		}
289 280
 		return scratchImage, nil
290 281
 	}
291
-	imageMount, err := b.imageSources.Get(name, localOnly)
282
+	imageMount, err := d.builder.imageSources.Get(name, localOnly)
292 283
 	if err != nil {
293 284
 		return nil, err
294 285
 	}
295 286
 	return imageMount.Image(), nil
296 287
 }
297
-
298
-func processOnBuild(req dispatchRequest) error {
299
-	dispatchState := req.state
300
-	// Process ONBUILD triggers if they exist
301
-	if nTriggers := len(dispatchState.runConfig.OnBuild); nTriggers != 0 {
302
-		word := "trigger"
303
-		if nTriggers > 1 {
304
-			word = "triggers"
305
-		}
306
-		fmt.Fprintf(req.builder.Stderr, "# Executing %d build %s...\n", nTriggers, word)
307
-	}
308
-
309
-	// Copy the ONBUILD triggers, and remove them from the config, since the config will be committed.
310
-	onBuildTriggers := dispatchState.runConfig.OnBuild
311
-	dispatchState.runConfig.OnBuild = []string{}
312
-
313
-	// Reset stdin settings as all build actions run without stdin
314
-	dispatchState.runConfig.OpenStdin = false
315
-	dispatchState.runConfig.StdinOnce = false
316
-
317
-	// parse the ONBUILD triggers by invoking the parser
318
-	for _, step := range onBuildTriggers {
319
-		dockerfile, err := parser.Parse(strings.NewReader(step))
320
-		if err != nil {
321
-			return err
322
-		}
323
-
324
-		for _, n := range dockerfile.AST.Children {
325
-			if err := checkDispatch(n); err != nil {
326
-				return err
327
-			}
328
-
329
-			upperCasedCmd := strings.ToUpper(n.Value)
330
-			switch upperCasedCmd {
331
-			case "ONBUILD":
332
-				return errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
333
-			case "MAINTAINER", "FROM":
334
-				return errors.Errorf("%s isn't allowed as an ONBUILD trigger", upperCasedCmd)
335
-			}
336
-		}
337
-
338
-		if _, err := dispatchFromDockerfile(req.builder, dockerfile, dispatchState, req.source); err != nil {
339
-			return err
340
-		}
288
+func (d *dispatchRequest) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
289
+	name, err := d.getExpandedImageName(shlex, name)
290
+	if err != nil {
291
+		return nil, err
341 292
 	}
342
-	return nil
293
+	return d.getImageOrStage(name)
343 294
 }
344 295
 
345
-// ONBUILD RUN echo yo
346
-//
347
-// ONBUILD triggers run when the image is used in a FROM statement.
348
-//
349
-// ONBUILD handling has a lot of special-case functionality, the heading in
350
-// evaluator.go and comments around dispatch() in the same file explain the
351
-// special cases. search for 'OnBuild' in internals.go for additional special
352
-// cases.
353
-//
354
-func onbuild(req dispatchRequest) error {
355
-	if len(req.args) == 0 {
356
-		return errAtLeastOneArgument("ONBUILD")
357
-	}
358
-
359
-	if err := req.flags.Parse(); err != nil {
360
-		return err
361
-	}
362
-
363
-	triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0]))
364
-	switch triggerInstruction {
365
-	case "ONBUILD":
366
-		return errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
367
-	case "MAINTAINER", "FROM":
368
-		return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
369
-	}
296
+func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error {
370 297
 
371
-	runConfig := req.state.runConfig
372
-	original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
373
-	runConfig.OnBuild = append(runConfig.OnBuild, original)
374
-	return req.builder.commit(req.state, "ONBUILD "+original)
298
+	d.state.runConfig.OnBuild = append(d.state.runConfig.OnBuild, c.Expression)
299
+	return d.builder.commit(d.state, "ONBUILD "+c.Expression)
375 300
 }
376 301
 
377 302
 // WORKDIR /tmp
378 303
 //
379 304
 // Set the working directory for future RUN/CMD/etc statements.
380 305
 //
381
-func workdir(req dispatchRequest) error {
382
-	if len(req.args) != 1 {
383
-		return errExactlyOneArgument("WORKDIR")
384
-	}
385
-
386
-	err := req.flags.Parse()
387
-	if err != nil {
388
-		return err
389
-	}
390
-
391
-	runConfig := req.state.runConfig
392
-	// This is from the Dockerfile and will not necessarily be in platform
393
-	// specific semantics, hence ensure it is converted.
394
-	runConfig.WorkingDir, err = normalizeWorkdir(req.builder.platform, runConfig.WorkingDir, req.args[0])
306
+func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error {
307
+	runConfig := d.state.runConfig
308
+	var err error
309
+	runConfig.WorkingDir, err = normalizeWorkdir(d.builder.platform, runConfig.WorkingDir, c.Path)
395 310
 	if err != nil {
396 311
 		return err
397 312
 	}
... ...
@@ -400,23 +263,31 @@ func workdir(req dispatchRequest) error {
400 400
 	// This avoids having an unnecessary expensive mount/unmount calls
401 401
 	// (on Windows in particular) during each container create.
402 402
 	// Prior to 1.13, the mkdir was deferred and not executed at this step.
403
-	if req.builder.disableCommit {
403
+	if d.builder.disableCommit {
404 404
 		// Don't call back into the daemon if we're going through docker commit --change "WORKDIR /foo".
405 405
 		// We've already updated the runConfig and that's enough.
406 406
 		return nil
407 407
 	}
408 408
 
409 409
 	comment := "WORKDIR " + runConfig.WorkingDir
410
-	runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, req.builder.platform))
411
-	containerID, err := req.builder.probeAndCreate(req.state, runConfigWithCommentCmd)
410
+	runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.builder.platform))
411
+	containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd)
412 412
 	if err != nil || containerID == "" {
413 413
 		return err
414 414
 	}
415
-	if err := req.builder.docker.ContainerCreateWorkdir(containerID); err != nil {
415
+	if err := d.builder.docker.ContainerCreateWorkdir(containerID); err != nil {
416 416
 		return err
417 417
 	}
418 418
 
419
-	return req.builder.commitContainer(req.state, containerID, runConfigWithCommentCmd)
419
+	return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd)
420
+}
421
+
422
+func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, platform string) []string {
423
+	result := cmd.CmdLine
424
+	if cmd.PrependShell && result != nil {
425
+		result = append(getShell(runConfig, platform), result...)
426
+	}
427
+	return result
420 428
 }
421 429
 
422 430
 // RUN some command yo
... ...
@@ -429,32 +300,21 @@ func workdir(req dispatchRequest) error {
429 429
 // RUN echo hi          # cmd /S /C echo hi   (Windows)
430 430
 // RUN [ "echo", "hi" ] # echo hi
431 431
 //
432
-func run(req dispatchRequest) error {
433
-	if !req.state.hasFromImage() {
434
-		return errors.New("Please provide a source image with `from` prior to run")
435
-	}
432
+func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {
436 433
 
437
-	if err := req.flags.Parse(); err != nil {
438
-		return err
439
-	}
440
-
441
-	stateRunConfig := req.state.runConfig
442
-	args := handleJSONArgs(req.args, req.attributes)
443
-	if !req.attributes["json"] {
444
-		args = append(getShell(stateRunConfig, req.builder.platform), args...)
445
-	}
446
-	cmdFromArgs := strslice.StrSlice(args)
447
-	buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env)
434
+	stateRunConfig := d.state.runConfig
435
+	cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.builder.platform)
436
+	buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env)
448 437
 
449 438
 	saveCmd := cmdFromArgs
450 439
 	if len(buildArgs) > 0 {
451
-		saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs)
440
+		saveCmd = prependEnvOnCmd(d.state.buildArgs, buildArgs, cmdFromArgs)
452 441
 	}
453 442
 
454 443
 	runConfigForCacheProbe := copyRunConfig(stateRunConfig,
455 444
 		withCmd(saveCmd),
456 445
 		withEntrypointOverride(saveCmd, nil))
457
-	hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe)
446
+	hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe)
458 447
 	if err != nil || hit {
459 448
 		return err
460 449
 	}
... ...
@@ -468,11 +328,11 @@ func run(req dispatchRequest) error {
468 468
 	runConfig.ArgsEscaped = true
469 469
 
470 470
 	logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)
471
-	cID, err := req.builder.create(runConfig)
471
+	cID, err := d.builder.create(runConfig)
472 472
 	if err != nil {
473 473
 		return err
474 474
 	}
475
-	if err := req.builder.containerManager.Run(req.builder.clientCtx, cID, req.builder.Stdout, req.builder.Stderr); err != nil {
475
+	if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil {
476 476
 		if err, ok := err.(*statusCodeError); ok {
477 477
 			// TODO: change error type, because jsonmessage.JSONError assumes HTTP
478 478
 			return &jsonmessage.JSONError{
... ...
@@ -485,7 +345,7 @@ func run(req dispatchRequest) error {
485 485
 		return err
486 486
 	}
487 487
 
488
-	return req.builder.commitContainer(req.state, cID, runConfigForCacheProbe)
488
+	return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe)
489 489
 }
490 490
 
491 491
 // Derive the command to use for probeCache() and to commit in this container.
... ...
@@ -518,139 +378,39 @@ func prependEnvOnCmd(buildArgs *buildArgs, buildArgVars []string, cmd strslice.S
518 518
 // Set the default command to run in the container (which may be empty).
519 519
 // Argument handling is the same as RUN.
520 520
 //
521
-func cmd(req dispatchRequest) error {
522
-	if err := req.flags.Parse(); err != nil {
523
-		return err
524
-	}
525
-
526
-	runConfig := req.state.runConfig
527
-	cmdSlice := handleJSONArgs(req.args, req.attributes)
528
-	if !req.attributes["json"] {
529
-		cmdSlice = append(getShell(runConfig, req.builder.platform), cmdSlice...)
530
-	}
531
-
532
-	runConfig.Cmd = strslice.StrSlice(cmdSlice)
521
+func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error {
522
+	runConfig := d.state.runConfig
523
+	cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.platform)
524
+	runConfig.Cmd = cmd
533 525
 	// set config as already being escaped, this prevents double escaping on windows
534 526
 	runConfig.ArgsEscaped = true
535 527
 
536
-	if err := req.builder.commit(req.state, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
528
+	if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil {
537 529
 		return err
538 530
 	}
539 531
 
540
-	if len(req.args) != 0 {
541
-		req.state.cmdSet = true
532
+	if len(c.ShellDependantCmdLine.CmdLine) != 0 {
533
+		d.state.cmdSet = true
542 534
 	}
543 535
 
544 536
 	return nil
545 537
 }
546 538
 
547
-// parseOptInterval(flag) is the duration of flag.Value, or 0 if
548
-// empty. An error is reported if the value is given and less than minimum duration.
549
-func parseOptInterval(f *Flag) (time.Duration, error) {
550
-	s := f.Value
551
-	if s == "" {
552
-		return 0, nil
553
-	}
554
-	d, err := time.ParseDuration(s)
555
-	if err != nil {
556
-		return 0, err
557
-	}
558
-	if d < container.MinimumDuration {
559
-		return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration)
560
-	}
561
-	return d, nil
562
-}
563
-
564 539
 // HEALTHCHECK foo
565 540
 //
566 541
 // Set the default healthcheck command to run in the container (which may be empty).
567 542
 // Argument handling is the same as RUN.
568 543
 //
569
-func healthcheck(req dispatchRequest) error {
570
-	if len(req.args) == 0 {
571
-		return errAtLeastOneArgument("HEALTHCHECK")
572
-	}
573
-	runConfig := req.state.runConfig
574
-	typ := strings.ToUpper(req.args[0])
575
-	args := req.args[1:]
576
-	if typ == "NONE" {
577
-		if len(args) != 0 {
578
-			return errors.New("HEALTHCHECK NONE takes no arguments")
579
-		}
580
-		test := strslice.StrSlice{typ}
581
-		runConfig.Healthcheck = &container.HealthConfig{
582
-			Test: test,
583
-		}
584
-	} else {
585
-		if runConfig.Healthcheck != nil {
586
-			oldCmd := runConfig.Healthcheck.Test
587
-			if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
588
-				fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd)
589
-			}
590
-		}
591
-
592
-		healthcheck := container.HealthConfig{}
593
-
594
-		flInterval := req.flags.AddString("interval", "")
595
-		flTimeout := req.flags.AddString("timeout", "")
596
-		flStartPeriod := req.flags.AddString("start-period", "")
597
-		flRetries := req.flags.AddString("retries", "")
598
-
599
-		if err := req.flags.Parse(); err != nil {
600
-			return err
601
-		}
602
-
603
-		switch typ {
604
-		case "CMD":
605
-			cmdSlice := handleJSONArgs(args, req.attributes)
606
-			if len(cmdSlice) == 0 {
607
-				return errors.New("Missing command after HEALTHCHECK CMD")
608
-			}
609
-
610
-			if !req.attributes["json"] {
611
-				typ = "CMD-SHELL"
612
-			}
613
-
614
-			healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...))
615
-		default:
616
-			return fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ)
617
-		}
618
-
619
-		interval, err := parseOptInterval(flInterval)
620
-		if err != nil {
621
-			return err
622
-		}
623
-		healthcheck.Interval = interval
624
-
625
-		timeout, err := parseOptInterval(flTimeout)
626
-		if err != nil {
627
-			return err
628
-		}
629
-		healthcheck.Timeout = timeout
630
-
631
-		startPeriod, err := parseOptInterval(flStartPeriod)
632
-		if err != nil {
633
-			return err
634
-		}
635
-		healthcheck.StartPeriod = startPeriod
636
-
637
-		if flRetries.Value != "" {
638
-			retries, err := strconv.ParseInt(flRetries.Value, 10, 32)
639
-			if err != nil {
640
-				return err
641
-			}
642
-			if retries < 1 {
643
-				return fmt.Errorf("--retries must be at least 1 (not %d)", retries)
644
-			}
645
-			healthcheck.Retries = int(retries)
646
-		} else {
647
-			healthcheck.Retries = 0
544
+func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) error {
545
+	runConfig := d.state.runConfig
546
+	if runConfig.Healthcheck != nil {
547
+		oldCmd := runConfig.Healthcheck.Test
548
+		if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
549
+			fmt.Fprintf(d.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd)
648 550
 		}
649
-
650
-		runConfig.Healthcheck = &healthcheck
651 551
 	}
652
-
653
-	return req.builder.commit(req.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck))
552
+	runConfig.Healthcheck = c.Health
553
+	return d.builder.commit(d.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck))
654 554
 }
655 555
 
656 556
 // ENTRYPOINT /usr/sbin/nginx
... ...
@@ -661,33 +421,15 @@ func healthcheck(req dispatchRequest) error {
661 661
 // Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint
662 662
 // is initialized at newBuilder time instead of through argument parsing.
663 663
 //
664
-func entrypoint(req dispatchRequest) error {
665
-	if err := req.flags.Parse(); err != nil {
666
-		return err
667
-	}
668
-
669
-	runConfig := req.state.runConfig
670
-	parsed := handleJSONArgs(req.args, req.attributes)
671
-
672
-	switch {
673
-	case req.attributes["json"]:
674
-		// ENTRYPOINT ["echo", "hi"]
675
-		runConfig.Entrypoint = strslice.StrSlice(parsed)
676
-	case len(parsed) == 0:
677
-		// ENTRYPOINT []
678
-		runConfig.Entrypoint = nil
679
-	default:
680
-		// ENTRYPOINT echo hi
681
-		runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig, req.builder.platform), parsed[0]))
682
-	}
683
-
684
-	// when setting the entrypoint if a CMD was not explicitly set then
685
-	// set the command to nil
686
-	if !req.state.cmdSet {
664
+func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error {
665
+	runConfig := d.state.runConfig
666
+	cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.platform)
667
+	runConfig.Entrypoint = cmd
668
+	if !d.state.cmdSet {
687 669
 		runConfig.Cmd = nil
688 670
 	}
689 671
 
690
-	return req.builder.commit(req.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint))
672
+	return d.builder.commit(d.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint))
691 673
 }
692 674
 
693 675
 // EXPOSE 6667/tcp 7000/tcp
... ...
@@ -695,41 +437,33 @@ func entrypoint(req dispatchRequest) error {
695 695
 // Expose ports for links and port mappings. This all ends up in
696 696
 // req.runConfig.ExposedPorts for runconfig.
697 697
 //
698
-func expose(req dispatchRequest) error {
699
-	portsTab := req.args
700
-
701
-	if len(req.args) == 0 {
702
-		return errAtLeastOneArgument("EXPOSE")
698
+func dispatchExpose(d dispatchRequest, c *instructions.ExposeCommand, envs []string) error {
699
+	// custom multi word expansion
700
+	// expose $FOO with FOO="80 443" is expanded as EXPOSE [80,443]. This is the only command supporting word to words expansion
701
+	// so the word processing has been de-generalized
702
+	ports := []string{}
703
+	for _, p := range c.Ports {
704
+		ps, err := d.shlex.ProcessWords(p, envs)
705
+		if err != nil {
706
+			return err
707
+		}
708
+		ports = append(ports, ps...)
703 709
 	}
710
+	c.Ports = ports
704 711
 
705
-	if err := req.flags.Parse(); err != nil {
712
+	ps, _, err := nat.ParsePortSpecs(ports)
713
+	if err != nil {
706 714
 		return err
707 715
 	}
708 716
 
709
-	runConfig := req.state.runConfig
710
-	if runConfig.ExposedPorts == nil {
711
-		runConfig.ExposedPorts = make(nat.PortSet)
717
+	if d.state.runConfig.ExposedPorts == nil {
718
+		d.state.runConfig.ExposedPorts = make(nat.PortSet)
712 719
 	}
713
-
714
-	ports, _, err := nat.ParsePortSpecs(portsTab)
715
-	if err != nil {
716
-		return err
720
+	for p := range ps {
721
+		d.state.runConfig.ExposedPorts[p] = struct{}{}
717 722
 	}
718 723
 
719
-	// instead of using ports directly, we build a list of ports and sort it so
720
-	// the order is consistent. This prevents cache burst where map ordering
721
-	// changes between builds
722
-	portList := make([]string, len(ports))
723
-	var i int
724
-	for port := range ports {
725
-		if _, exists := runConfig.ExposedPorts[port]; !exists {
726
-			runConfig.ExposedPorts[port] = struct{}{}
727
-		}
728
-		portList[i] = string(port)
729
-		i++
730
-	}
731
-	sort.Strings(portList)
732
-	return req.builder.commit(req.state, "EXPOSE "+strings.Join(portList, " "))
724
+	return d.builder.commit(d.state, "EXPOSE "+strings.Join(c.Ports, " "))
733 725
 }
734 726
 
735 727
 // USER foo
... ...
@@ -737,62 +471,39 @@ func expose(req dispatchRequest) error {
737 737
 // Set the user to 'foo' for future commands and when running the
738 738
 // ENTRYPOINT/CMD at container run time.
739 739
 //
740
-func user(req dispatchRequest) error {
741
-	if len(req.args) != 1 {
742
-		return errExactlyOneArgument("USER")
743
-	}
744
-
745
-	if err := req.flags.Parse(); err != nil {
746
-		return err
747
-	}
748
-
749
-	req.state.runConfig.User = req.args[0]
750
-	return req.builder.commit(req.state, fmt.Sprintf("USER %v", req.args))
740
+func dispatchUser(d dispatchRequest, c *instructions.UserCommand) error {
741
+	d.state.runConfig.User = c.User
742
+	return d.builder.commit(d.state, fmt.Sprintf("USER %v", c.User))
751 743
 }
752 744
 
753 745
 // VOLUME /foo
754 746
 //
755 747
 // Expose the volume /foo for use. Will also accept the JSON array form.
756 748
 //
757
-func volume(req dispatchRequest) error {
758
-	if len(req.args) == 0 {
759
-		return errAtLeastOneArgument("VOLUME")
760
-	}
761
-
762
-	if err := req.flags.Parse(); err != nil {
763
-		return err
749
+func dispatchVolume(d dispatchRequest, c *instructions.VolumeCommand) error {
750
+	if d.state.runConfig.Volumes == nil {
751
+		d.state.runConfig.Volumes = map[string]struct{}{}
764 752
 	}
765
-
766
-	runConfig := req.state.runConfig
767
-	if runConfig.Volumes == nil {
768
-		runConfig.Volumes = map[string]struct{}{}
769
-	}
770
-	for _, v := range req.args {
771
-		v = strings.TrimSpace(v)
753
+	for _, v := range c.Volumes {
772 754
 		if v == "" {
773 755
 			return errors.New("VOLUME specified can not be an empty string")
774 756
 		}
775
-		runConfig.Volumes[v] = struct{}{}
757
+		d.state.runConfig.Volumes[v] = struct{}{}
776 758
 	}
777
-	return req.builder.commit(req.state, fmt.Sprintf("VOLUME %v", req.args))
759
+	return d.builder.commit(d.state, fmt.Sprintf("VOLUME %v", c.Volumes))
778 760
 }
779 761
 
780 762
 // STOPSIGNAL signal
781 763
 //
782 764
 // Set the signal that will be used to kill the container.
783
-func stopSignal(req dispatchRequest) error {
784
-	if len(req.args) != 1 {
785
-		return errExactlyOneArgument("STOPSIGNAL")
786
-	}
765
+func dispatchStopSignal(d dispatchRequest, c *instructions.StopSignalCommand) error {
787 766
 
788
-	sig := req.args[0]
789
-	_, err := signal.ParseSignal(sig)
767
+	_, err := signal.ParseSignal(c.Signal)
790 768
 	if err != nil {
791 769
 		return validationError{err}
792 770
 	}
793
-
794
-	req.state.runConfig.StopSignal = sig
795
-	return req.builder.commit(req.state, fmt.Sprintf("STOPSIGNAL %v", req.args))
771
+	d.state.runConfig.StopSignal = c.Signal
772
+	return d.builder.commit(d.state, fmt.Sprintf("STOPSIGNAL %v", c.Signal))
796 773
 }
797 774
 
798 775
 // ARG name[=value]
... ...
@@ -800,89 +511,21 @@ func stopSignal(req dispatchRequest) error {
800 800
 // Adds the variable foo to the trusted list of variables that can be passed
801 801
 // to builder using the --build-arg flag for expansion/substitution or passing to 'run'.
802 802
 // Dockerfile author may optionally set a default value of this variable.
803
-func arg(req dispatchRequest) error {
804
-	if len(req.args) != 1 {
805
-		return errExactlyOneArgument("ARG")
806
-	}
807
-
808
-	var (
809
-		name       string
810
-		newValue   string
811
-		hasDefault bool
812
-	)
813
-
814
-	arg := req.args[0]
815
-	// 'arg' can just be a name or name-value pair. Note that this is different
816
-	// from 'env' that handles the split of name and value at the parser level.
817
-	// The reason for doing it differently for 'arg' is that we support just
818
-	// defining an arg and not assign it a value (while 'env' always expects a
819
-	// name-value pair). If possible, it will be good to harmonize the two.
820
-	if strings.Contains(arg, "=") {
821
-		parts := strings.SplitN(arg, "=", 2)
822
-		if len(parts[0]) == 0 {
823
-			return errBlankCommandNames("ARG")
824
-		}
825
-
826
-		name = parts[0]
827
-		newValue = parts[1]
828
-		hasDefault = true
829
-	} else {
830
-		name = arg
831
-		hasDefault = false
832
-	}
803
+func dispatchArg(d dispatchRequest, c *instructions.ArgCommand) error {
833 804
 
834
-	var value *string
835
-	if hasDefault {
836
-		value = &newValue
805
+	commitStr := "ARG " + c.Key
806
+	if c.Value != nil {
807
+		commitStr += "=" + *c.Value
837 808
 	}
838
-	req.builder.buildArgs.AddArg(name, value)
839 809
 
840
-	// Arg before FROM doesn't add a layer
841
-	if !req.state.hasFromImage() {
842
-		req.builder.buildArgs.AddMetaArg(name, value)
843
-		return nil
844
-	}
845
-	return req.builder.commit(req.state, "ARG "+arg)
810
+	d.state.buildArgs.AddArg(c.Key, c.Value)
811
+	return d.builder.commit(d.state, commitStr)
846 812
 }
847 813
 
848 814
 // SHELL powershell -command
849 815
 //
850 816
 // Set the non-default shell to use.
851
-func shell(req dispatchRequest) error {
852
-	if err := req.flags.Parse(); err != nil {
853
-		return err
854
-	}
855
-	shellSlice := handleJSONArgs(req.args, req.attributes)
856
-	switch {
857
-	case len(shellSlice) == 0:
858
-		// SHELL []
859
-		return errAtLeastOneArgument("SHELL")
860
-	case req.attributes["json"]:
861
-		// SHELL ["powershell", "-command"]
862
-		req.state.runConfig.Shell = strslice.StrSlice(shellSlice)
863
-	default:
864
-		// SHELL powershell -command - not JSON
865
-		return errNotJSON("SHELL", req.original)
866
-	}
867
-	return req.builder.commit(req.state, fmt.Sprintf("SHELL %v", shellSlice))
868
-}
869
-
870
-func errAtLeastOneArgument(command string) error {
871
-	return fmt.Errorf("%s requires at least one argument", command)
872
-}
873
-
874
-func errExactlyOneArgument(command string) error {
875
-	return fmt.Errorf("%s requires exactly one argument", command)
876
-}
877
-
878
-func errAtLeastTwoArguments(command string) error {
879
-	return fmt.Errorf("%s requires at least two arguments", command)
880
-}
881
-
882
-func errBlankCommandNames(command string) error {
883
-	return fmt.Errorf("%s names can not be blank", command)
884
-}
885
-
886
-func errTooManyArguments(command string) error {
887
-	return fmt.Errorf("Bad input to %s, too many arguments", command)
817
+func dispatchShell(d dispatchRequest, c *instructions.ShellCommand) error {
818
+	d.state.runConfig.Shell = c.Shell
819
+	return d.builder.commit(d.state, fmt.Sprintf("SHELL %v", d.state.runConfig.Shell))
888 820
 }
... ...
@@ -1,60 +1,29 @@
1 1
 package dockerfile
2 2
 
3 3
 import (
4
-	"fmt"
5
-	"runtime"
6
-	"testing"
7
-
8 4
 	"bytes"
9 5
 	"context"
6
+	"runtime"
7
+	"testing"
10 8
 
11 9
 	"github.com/docker/docker/api/types"
12 10
 	"github.com/docker/docker/api/types/backend"
13 11
 	"github.com/docker/docker/api/types/container"
14 12
 	"github.com/docker/docker/api/types/strslice"
15 13
 	"github.com/docker/docker/builder"
16
-	"github.com/docker/docker/builder/dockerfile/parser"
17
-	"github.com/docker/docker/internal/testutil"
14
+	"github.com/docker/docker/builder/dockerfile/instructions"
18 15
 	"github.com/docker/docker/pkg/system"
19 16
 	"github.com/docker/go-connections/nat"
20 17
 	"github.com/stretchr/testify/assert"
21 18
 	"github.com/stretchr/testify/require"
22 19
 )
23 20
 
24
-type commandWithFunction struct {
25
-	name     string
26
-	function func(args []string) error
27
-}
28
-
29
-func withArgs(f dispatcher) func([]string) error {
30
-	return func(args []string) error {
31
-		return f(dispatchRequest{args: args})
32
-	}
33
-}
34
-
35
-func withBuilderAndArgs(builder *Builder, f dispatcher) func([]string) error {
36
-	return func(args []string) error {
37
-		return f(defaultDispatchReq(builder, args...))
38
-	}
39
-}
40
-
41
-func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest {
42
-	return dispatchRequest{
43
-		builder: builder,
44
-		args:    args,
45
-		flags:   NewBFlags(),
46
-		shlex:   NewShellLex(parser.DefaultEscapeToken),
47
-		state:   &dispatchState{runConfig: &container.Config{}},
48
-	}
49
-}
50
-
51 21
 func newBuilderWithMockBackend() *Builder {
52 22
 	mockBackend := &MockBackend{}
53 23
 	ctx := context.Background()
54 24
 	b := &Builder{
55 25
 		options:       &types.ImageBuildOptions{},
56 26
 		docker:        mockBackend,
57
-		buildArgs:     newBuildArgs(make(map[string]*string)),
58 27
 		Stdout:        new(bytes.Buffer),
59 28
 		clientCtx:     ctx,
60 29
 		disableCommit: true,
... ...
@@ -62,137 +31,84 @@ func newBuilderWithMockBackend() *Builder {
62 62
 			Options: &types.ImageBuildOptions{},
63 63
 			Backend: mockBackend,
64 64
 		}),
65
-		buildStages:      newBuildStages(),
66 65
 		imageProber:      newImageProber(mockBackend, nil, runtime.GOOS, false),
67 66
 		containerManager: newContainerManager(mockBackend),
68 67
 	}
69 68
 	return b
70 69
 }
71 70
 
72
-func TestCommandsExactlyOneArgument(t *testing.T) {
73
-	commands := []commandWithFunction{
74
-		{"MAINTAINER", withArgs(maintainer)},
75
-		{"WORKDIR", withArgs(workdir)},
76
-		{"USER", withArgs(user)},
77
-		{"STOPSIGNAL", withArgs(stopSignal)},
78
-	}
79
-
80
-	for _, command := range commands {
81
-		err := command.function([]string{})
82
-		assert.EqualError(t, err, errExactlyOneArgument(command.name).Error())
83
-	}
84
-}
85
-
86
-func TestCommandsAtLeastOneArgument(t *testing.T) {
87
-	commands := []commandWithFunction{
88
-		{"ENV", withArgs(env)},
89
-		{"LABEL", withArgs(label)},
90
-		{"ONBUILD", withArgs(onbuild)},
91
-		{"HEALTHCHECK", withArgs(healthcheck)},
92
-		{"EXPOSE", withArgs(expose)},
93
-		{"VOLUME", withArgs(volume)},
94
-	}
95
-
96
-	for _, command := range commands {
97
-		err := command.function([]string{})
98
-		assert.EqualError(t, err, errAtLeastOneArgument(command.name).Error())
99
-	}
100
-}
101
-
102
-func TestCommandsAtLeastTwoArguments(t *testing.T) {
103
-	commands := []commandWithFunction{
104
-		{"ADD", withArgs(add)},
105
-		{"COPY", withArgs(dispatchCopy)}}
106
-
107
-	for _, command := range commands {
108
-		err := command.function([]string{"arg1"})
109
-		assert.EqualError(t, err, errAtLeastTwoArguments(command.name).Error())
110
-	}
111
-}
112
-
113
-func TestCommandsTooManyArguments(t *testing.T) {
114
-	commands := []commandWithFunction{
115
-		{"ENV", withArgs(env)},
116
-		{"LABEL", withArgs(label)}}
117
-
118
-	for _, command := range commands {
119
-		err := command.function([]string{"arg1", "arg2", "arg3"})
120
-		assert.EqualError(t, err, errTooManyArguments(command.name).Error())
121
-	}
122
-}
123
-
124
-func TestCommandsBlankNames(t *testing.T) {
125
-	builder := newBuilderWithMockBackend()
126
-	commands := []commandWithFunction{
127
-		{"ENV", withBuilderAndArgs(builder, env)},
128
-		{"LABEL", withBuilderAndArgs(builder, label)},
129
-	}
130
-
131
-	for _, command := range commands {
132
-		err := command.function([]string{"", ""})
133
-		assert.EqualError(t, err, errBlankCommandNames(command.name).Error())
134
-	}
135
-}
136
-
137 71
 func TestEnv2Variables(t *testing.T) {
138 72
 	b := newBuilderWithMockBackend()
139
-
140
-	args := []string{"var1", "val1", "var2", "val2"}
141
-	req := defaultDispatchReq(b, args...)
142
-	err := env(req)
73
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
74
+	envCommand := &instructions.EnvCommand{
75
+		Env: instructions.KeyValuePairs{
76
+			instructions.KeyValuePair{Key: "var1", Value: "val1"},
77
+			instructions.KeyValuePair{Key: "var2", Value: "val2"},
78
+		},
79
+	}
80
+	err := dispatch(sb, envCommand)
143 81
 	require.NoError(t, err)
144 82
 
145 83
 	expected := []string{
146
-		fmt.Sprintf("%s=%s", args[0], args[1]),
147
-		fmt.Sprintf("%s=%s", args[2], args[3]),
84
+		"var1=val1",
85
+		"var2=val2",
148 86
 	}
149
-	assert.Equal(t, expected, req.state.runConfig.Env)
87
+	assert.Equal(t, expected, sb.state.runConfig.Env)
150 88
 }
151 89
 
152 90
 func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
153 91
 	b := newBuilderWithMockBackend()
154
-
155
-	args := []string{"var1", "val1"}
156
-	req := defaultDispatchReq(b, args...)
157
-	req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
158
-	err := env(req)
92
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
93
+	sb.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
94
+	envCommand := &instructions.EnvCommand{
95
+		Env: instructions.KeyValuePairs{
96
+			instructions.KeyValuePair{Key: "var1", Value: "val1"},
97
+		},
98
+	}
99
+	err := dispatch(sb, envCommand)
159 100
 	require.NoError(t, err)
160
-
161 101
 	expected := []string{
162
-		fmt.Sprintf("%s=%s", args[0], args[1]),
102
+		"var1=val1",
163 103
 		"var2=fromenv",
164 104
 	}
165
-	assert.Equal(t, expected, req.state.runConfig.Env)
105
+	assert.Equal(t, expected, sb.state.runConfig.Env)
166 106
 }
167 107
 
168 108
 func TestMaintainer(t *testing.T) {
169 109
 	maintainerEntry := "Some Maintainer <maintainer@example.com>"
170
-
171 110
 	b := newBuilderWithMockBackend()
172
-	req := defaultDispatchReq(b, maintainerEntry)
173
-	err := maintainer(req)
111
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
112
+	cmd := &instructions.MaintainerCommand{Maintainer: maintainerEntry}
113
+	err := dispatch(sb, cmd)
174 114
 	require.NoError(t, err)
175
-	assert.Equal(t, maintainerEntry, req.state.maintainer)
115
+	assert.Equal(t, maintainerEntry, sb.state.maintainer)
176 116
 }
177 117
 
178 118
 func TestLabel(t *testing.T) {
179 119
 	labelName := "label"
180 120
 	labelValue := "value"
181 121
 
182
-	labelEntry := []string{labelName, labelValue}
183 122
 	b := newBuilderWithMockBackend()
184
-	req := defaultDispatchReq(b, labelEntry...)
185
-	err := label(req)
123
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
124
+	cmd := &instructions.LabelCommand{
125
+		Labels: instructions.KeyValuePairs{
126
+			instructions.KeyValuePair{Key: labelName, Value: labelValue},
127
+		},
128
+	}
129
+	err := dispatch(sb, cmd)
186 130
 	require.NoError(t, err)
187 131
 
188
-	require.Contains(t, req.state.runConfig.Labels, labelName)
189
-	assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue)
132
+	require.Contains(t, sb.state.runConfig.Labels, labelName)
133
+	assert.Equal(t, sb.state.runConfig.Labels[labelName], labelValue)
190 134
 }
191 135
 
192 136
 func TestFromScratch(t *testing.T) {
193 137
 	b := newBuilderWithMockBackend()
194
-	req := defaultDispatchReq(b, "scratch")
195
-	err := from(req)
138
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
139
+	cmd := &instructions.Stage{
140
+		BaseName: "scratch",
141
+	}
142
+	err := initializeStage(sb, cmd)
196 143
 
197 144
 	if runtime.GOOS == "windows" && !system.LCOWSupported() {
198 145
 		assert.EqualError(t, err, "Windows does not support FROM scratch")
... ...
@@ -200,14 +116,14 @@ func TestFromScratch(t *testing.T) {
200 200
 	}
201 201
 
202 202
 	require.NoError(t, err)
203
-	assert.True(t, req.state.hasFromImage())
204
-	assert.Equal(t, "", req.state.imageID)
203
+	assert.True(t, sb.state.hasFromImage())
204
+	assert.Equal(t, "", sb.state.imageID)
205 205
 	// Windows does not set the default path. TODO @jhowardmsft LCOW support. This will need revisiting as we get further into the implementation
206 206
 	expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS)
207 207
 	if runtime.GOOS == "windows" {
208 208
 		expected = ""
209 209
 	}
210
-	assert.Equal(t, []string{expected}, req.state.runConfig.Env)
210
+	assert.Equal(t, []string{expected}, sb.state.runConfig.Env)
211 211
 }
212 212
 
213 213
 func TestFromWithArg(t *testing.T) {
... ...
@@ -219,16 +135,27 @@ func TestFromWithArg(t *testing.T) {
219 219
 	}
220 220
 	b := newBuilderWithMockBackend()
221 221
 	b.docker.(*MockBackend).getImageFunc = getImage
222
+	args := newBuildArgs(make(map[string]*string))
222 223
 
223
-	require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag)))
224
-	req := defaultDispatchReq(b, "alpine${THETAG}")
225
-	err := from(req)
224
+	val := "sometag"
225
+	metaArg := instructions.ArgCommand{
226
+		Key:   "THETAG",
227
+		Value: &val,
228
+	}
229
+	cmd := &instructions.Stage{
230
+		BaseName: "alpine:${THETAG}",
231
+	}
232
+	err := processMetaArg(metaArg, NewShellLex('\\'), args)
226 233
 
234
+	sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults())
227 235
 	require.NoError(t, err)
228
-	assert.Equal(t, expected, req.state.imageID)
229
-	assert.Equal(t, expected, req.state.baseImage.ImageID())
230
-	assert.Len(t, b.buildArgs.GetAllAllowed(), 0)
231
-	assert.Len(t, b.buildArgs.GetAllMeta(), 1)
236
+	err = initializeStage(sb, cmd)
237
+	require.NoError(t, err)
238
+
239
+	assert.Equal(t, expected, sb.state.imageID)
240
+	assert.Equal(t, expected, sb.state.baseImage.ImageID())
241
+	assert.Len(t, sb.state.buildArgs.GetAllAllowed(), 0)
242
+	assert.Len(t, sb.state.buildArgs.GetAllMeta(), 1)
232 243
 }
233 244
 
234 245
 func TestFromWithUndefinedArg(t *testing.T) {
... ...
@@ -240,74 +167,74 @@ func TestFromWithUndefinedArg(t *testing.T) {
240 240
 	}
241 241
 	b := newBuilderWithMockBackend()
242 242
 	b.docker.(*MockBackend).getImageFunc = getImage
243
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
244
+
243 245
 	b.options.BuildArgs = map[string]*string{"THETAG": &tag}
244 246
 
245
-	req := defaultDispatchReq(b, "alpine${THETAG}")
246
-	err := from(req)
247
+	cmd := &instructions.Stage{
248
+		BaseName: "alpine${THETAG}",
249
+	}
250
+	err := initializeStage(sb, cmd)
247 251
 	require.NoError(t, err)
248
-	assert.Equal(t, expected, req.state.imageID)
252
+	assert.Equal(t, expected, sb.state.imageID)
249 253
 }
250 254
 
251
-func TestFromMultiStageWithScratchNamedStage(t *testing.T) {
252
-	if runtime.GOOS == "windows" {
253
-		t.Skip("Windows does not support scratch")
254
-	}
255
+func TestFromMultiStageWithNamedStage(t *testing.T) {
255 256
 	b := newBuilderWithMockBackend()
256
-	req := defaultDispatchReq(b, "scratch", "AS", "base")
257
-
258
-	require.NoError(t, from(req))
259
-	assert.True(t, req.state.hasFromImage())
260
-
261
-	req.args = []string{"base"}
262
-	require.NoError(t, from(req))
263
-	assert.True(t, req.state.hasFromImage())
264
-}
265
-
266
-func TestOnbuildIllegalTriggers(t *testing.T) {
267
-	triggers := []struct{ command, expectedError string }{
268
-		{"ONBUILD", "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed"},
269
-		{"MAINTAINER", "MAINTAINER isn't allowed as an ONBUILD trigger"},
270
-		{"FROM", "FROM isn't allowed as an ONBUILD trigger"}}
271
-
272
-	for _, trigger := range triggers {
273
-		b := newBuilderWithMockBackend()
274
-
275
-		err := onbuild(defaultDispatchReq(b, trigger.command))
276
-		testutil.ErrorContains(t, err, trigger.expectedError)
277
-	}
257
+	firstFrom := &instructions.Stage{BaseName: "someimg", Name: "base"}
258
+	secondFrom := &instructions.Stage{BaseName: "base"}
259
+	previousResults := newStagesBuildResults()
260
+	firstSB := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), previousResults)
261
+	secondSB := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), previousResults)
262
+	err := initializeStage(firstSB, firstFrom)
263
+	require.NoError(t, err)
264
+	assert.True(t, firstSB.state.hasFromImage())
265
+	previousResults.indexed["base"] = firstSB.state.runConfig
266
+	previousResults.flat = append(previousResults.flat, firstSB.state.runConfig)
267
+	err = initializeStage(secondSB, secondFrom)
268
+	require.NoError(t, err)
269
+	assert.True(t, secondSB.state.hasFromImage())
278 270
 }
279 271
 
280 272
 func TestOnbuild(t *testing.T) {
281 273
 	b := newBuilderWithMockBackend()
282
-
283
-	req := defaultDispatchReq(b, "ADD", ".", "/app/src")
284
-	req.original = "ONBUILD ADD . /app/src"
285
-	req.state.runConfig = &container.Config{}
286
-
287
-	err := onbuild(req)
274
+	sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
275
+	cmd := &instructions.OnbuildCommand{
276
+		Expression: "ADD . /app/src",
277
+	}
278
+	err := dispatch(sb, cmd)
288 279
 	require.NoError(t, err)
289
-	assert.Equal(t, "ADD . /app/src", req.state.runConfig.OnBuild[0])
280
+	assert.Equal(t, "ADD . /app/src", sb.state.runConfig.OnBuild[0])
290 281
 }
291 282
 
292 283
 func TestWorkdir(t *testing.T) {
293 284
 	b := newBuilderWithMockBackend()
285
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
294 286
 	workingDir := "/app"
295 287
 	if runtime.GOOS == "windows" {
296
-		workingDir = "C:\app"
288
+		workingDir = "C:\\app"
289
+	}
290
+	cmd := &instructions.WorkdirCommand{
291
+		Path: workingDir,
297 292
 	}
298 293
 
299
-	req := defaultDispatchReq(b, workingDir)
300
-	err := workdir(req)
294
+	err := dispatch(sb, cmd)
301 295
 	require.NoError(t, err)
302
-	assert.Equal(t, workingDir, req.state.runConfig.WorkingDir)
296
+	assert.Equal(t, workingDir, sb.state.runConfig.WorkingDir)
303 297
 }
304 298
 
305 299
 func TestCmd(t *testing.T) {
306 300
 	b := newBuilderWithMockBackend()
301
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
307 302
 	command := "./executable"
308 303
 
309
-	req := defaultDispatchReq(b, command)
310
-	err := cmd(req)
304
+	cmd := &instructions.CmdCommand{
305
+		ShellDependantCmdLine: instructions.ShellDependantCmdLine{
306
+			CmdLine:      strslice.StrSlice{command},
307
+			PrependShell: true,
308
+		},
309
+	}
310
+	err := dispatch(sb, cmd)
311 311
 	require.NoError(t, err)
312 312
 
313 313
 	var expectedCommand strslice.StrSlice
... ...
@@ -317,42 +244,56 @@ func TestCmd(t *testing.T) {
317 317
 		expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command))
318 318
 	}
319 319
 
320
-	assert.Equal(t, expectedCommand, req.state.runConfig.Cmd)
321
-	assert.True(t, req.state.cmdSet)
320
+	assert.Equal(t, expectedCommand, sb.state.runConfig.Cmd)
321
+	assert.True(t, sb.state.cmdSet)
322 322
 }
323 323
 
324 324
 func TestHealthcheckNone(t *testing.T) {
325 325
 	b := newBuilderWithMockBackend()
326
-
327
-	req := defaultDispatchReq(b, "NONE")
328
-	err := healthcheck(req)
326
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
327
+	cmd := &instructions.HealthCheckCommand{
328
+		Health: &container.HealthConfig{
329
+			Test: []string{"NONE"},
330
+		},
331
+	}
332
+	err := dispatch(sb, cmd)
329 333
 	require.NoError(t, err)
330 334
 
331
-	require.NotNil(t, req.state.runConfig.Healthcheck)
332
-	assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test)
335
+	require.NotNil(t, sb.state.runConfig.Healthcheck)
336
+	assert.Equal(t, []string{"NONE"}, sb.state.runConfig.Healthcheck.Test)
333 337
 }
334 338
 
335 339
 func TestHealthcheckCmd(t *testing.T) {
336
-	b := newBuilderWithMockBackend()
337 340
 
338
-	args := []string{"CMD", "curl", "-f", "http://localhost/", "||", "exit", "1"}
339
-	req := defaultDispatchReq(b, args...)
340
-	err := healthcheck(req)
341
+	b := newBuilderWithMockBackend()
342
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
343
+	expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
344
+	cmd := &instructions.HealthCheckCommand{
345
+		Health: &container.HealthConfig{
346
+			Test: expectedTest,
347
+		},
348
+	}
349
+	err := dispatch(sb, cmd)
341 350
 	require.NoError(t, err)
342 351
 
343
-	require.NotNil(t, req.state.runConfig.Healthcheck)
344
-	expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
345
-	assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test)
352
+	require.NotNil(t, sb.state.runConfig.Healthcheck)
353
+	assert.Equal(t, expectedTest, sb.state.runConfig.Healthcheck.Test)
346 354
 }
347 355
 
348 356
 func TestEntrypoint(t *testing.T) {
349 357
 	b := newBuilderWithMockBackend()
358
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
350 359
 	entrypointCmd := "/usr/sbin/nginx"
351 360
 
352
-	req := defaultDispatchReq(b, entrypointCmd)
353
-	err := entrypoint(req)
361
+	cmd := &instructions.EntrypointCommand{
362
+		ShellDependantCmdLine: instructions.ShellDependantCmdLine{
363
+			CmdLine:      strslice.StrSlice{entrypointCmd},
364
+			PrependShell: true,
365
+		},
366
+	}
367
+	err := dispatch(sb, cmd)
354 368
 	require.NoError(t, err)
355
-	require.NotNil(t, req.state.runConfig.Entrypoint)
369
+	require.NotNil(t, sb.state.runConfig.Entrypoint)
356 370
 
357 371
 	var expectedEntrypoint strslice.StrSlice
358 372
 	if runtime.GOOS == "windows" {
... ...
@@ -360,99 +301,99 @@ func TestEntrypoint(t *testing.T) {
360 360
 	} else {
361 361
 		expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd))
362 362
 	}
363
-	assert.Equal(t, expectedEntrypoint, req.state.runConfig.Entrypoint)
363
+	assert.Equal(t, expectedEntrypoint, sb.state.runConfig.Entrypoint)
364 364
 }
365 365
 
366 366
 func TestExpose(t *testing.T) {
367 367
 	b := newBuilderWithMockBackend()
368
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
368 369
 
369 370
 	exposedPort := "80"
370
-	req := defaultDispatchReq(b, exposedPort)
371
-	err := expose(req)
371
+	cmd := &instructions.ExposeCommand{
372
+		Ports: []string{exposedPort},
373
+	}
374
+	err := dispatch(sb, cmd)
372 375
 	require.NoError(t, err)
373 376
 
374
-	require.NotNil(t, req.state.runConfig.ExposedPorts)
375
-	require.Len(t, req.state.runConfig.ExposedPorts, 1)
377
+	require.NotNil(t, sb.state.runConfig.ExposedPorts)
378
+	require.Len(t, sb.state.runConfig.ExposedPorts, 1)
376 379
 
377 380
 	portsMapping, err := nat.ParsePortSpec(exposedPort)
378 381
 	require.NoError(t, err)
379
-	assert.Contains(t, req.state.runConfig.ExposedPorts, portsMapping[0].Port)
382
+	assert.Contains(t, sb.state.runConfig.ExposedPorts, portsMapping[0].Port)
380 383
 }
381 384
 
382 385
 func TestUser(t *testing.T) {
383 386
 	b := newBuilderWithMockBackend()
384
-	userCommand := "foo"
387
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
385 388
 
386
-	req := defaultDispatchReq(b, userCommand)
387
-	err := user(req)
389
+	cmd := &instructions.UserCommand{
390
+		User: "test",
391
+	}
392
+	err := dispatch(sb, cmd)
388 393
 	require.NoError(t, err)
389
-	assert.Equal(t, userCommand, req.state.runConfig.User)
394
+	assert.Equal(t, "test", sb.state.runConfig.User)
390 395
 }
391 396
 
392 397
 func TestVolume(t *testing.T) {
393 398
 	b := newBuilderWithMockBackend()
399
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
394 400
 
395 401
 	exposedVolume := "/foo"
396 402
 
397
-	req := defaultDispatchReq(b, exposedVolume)
398
-	err := volume(req)
403
+	cmd := &instructions.VolumeCommand{
404
+		Volumes: []string{exposedVolume},
405
+	}
406
+	err := dispatch(sb, cmd)
399 407
 	require.NoError(t, err)
400
-
401
-	require.NotNil(t, req.state.runConfig.Volumes)
402
-	assert.Len(t, req.state.runConfig.Volumes, 1)
403
-	assert.Contains(t, req.state.runConfig.Volumes, exposedVolume)
408
+	require.NotNil(t, sb.state.runConfig.Volumes)
409
+	assert.Len(t, sb.state.runConfig.Volumes, 1)
410
+	assert.Contains(t, sb.state.runConfig.Volumes, exposedVolume)
404 411
 }
405 412
 
406 413
 func TestStopSignal(t *testing.T) {
414
+	if runtime.GOOS == "windows" {
415
+		t.Skip("Windows does not support stopsignal")
416
+		return
417
+	}
407 418
 	b := newBuilderWithMockBackend()
419
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
408 420
 	signal := "SIGKILL"
409 421
 
410
-	req := defaultDispatchReq(b, signal)
411
-	err := stopSignal(req)
422
+	cmd := &instructions.StopSignalCommand{
423
+		Signal: signal,
424
+	}
425
+	err := dispatch(sb, cmd)
412 426
 	require.NoError(t, err)
413
-	assert.Equal(t, signal, req.state.runConfig.StopSignal)
427
+	assert.Equal(t, signal, sb.state.runConfig.StopSignal)
414 428
 }
415 429
 
416 430
 func TestArg(t *testing.T) {
417 431
 	b := newBuilderWithMockBackend()
432
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
418 433
 
419 434
 	argName := "foo"
420 435
 	argVal := "bar"
421
-	argDef := fmt.Sprintf("%s=%s", argName, argVal)
422
-
423
-	err := arg(defaultDispatchReq(b, argDef))
436
+	cmd := &instructions.ArgCommand{Key: argName, Value: &argVal}
437
+	err := dispatch(sb, cmd)
424 438
 	require.NoError(t, err)
425 439
 
426 440
 	expected := map[string]string{argName: argVal}
427
-	assert.Equal(t, expected, b.buildArgs.GetAllAllowed())
441
+	assert.Equal(t, expected, sb.state.buildArgs.GetAllAllowed())
428 442
 }
429 443
 
430 444
 func TestShell(t *testing.T) {
431 445
 	b := newBuilderWithMockBackend()
446
+	sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
432 447
 
433 448
 	shellCmd := "powershell"
434
-	req := defaultDispatchReq(b, shellCmd)
435
-	req.attributes = map[string]bool{"json": true}
449
+	cmd := &instructions.ShellCommand{Shell: strslice.StrSlice{shellCmd}}
436 450
 
437
-	err := shell(req)
451
+	err := dispatch(sb, cmd)
438 452
 	require.NoError(t, err)
439 453
 
440 454
 	expectedShell := strslice.StrSlice([]string{shellCmd})
441
-	assert.Equal(t, expectedShell, req.state.runConfig.Shell)
442
-}
443
-
444
-func TestParseOptInterval(t *testing.T) {
445
-	flInterval := &Flag{
446
-		name:     "interval",
447
-		flagType: stringType,
448
-		Value:    "50ns",
449
-	}
450
-	_, err := parseOptInterval(flInterval)
451
-	testutil.ErrorContains(t, err, "cannot be less than 1ms")
452
-
453
-	flInterval.Value = "1ms"
454
-	_, err = parseOptInterval(flInterval)
455
-	require.NoError(t, err)
455
+	assert.Equal(t, expectedShell, sb.state.runConfig.Shell)
456 456
 }
457 457
 
458 458
 func TestPrependEnvOnCmd(t *testing.T) {
... ...
@@ -469,8 +410,10 @@ func TestPrependEnvOnCmd(t *testing.T) {
469 469
 
470 470
 func TestRunWithBuildArgs(t *testing.T) {
471 471
 	b := newBuilderWithMockBackend()
472
-	b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
472
+	args := newBuildArgs(make(map[string]*string))
473
+	args.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
473 474
 	b.disableCommit = false
475
+	sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults())
474 476
 
475 477
 	runConfig := &container.Config{}
476 478
 	origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
... ...
@@ -512,14 +455,18 @@ func TestRunWithBuildArgs(t *testing.T) {
512 512
 		assert.Equal(t, strslice.StrSlice(nil), cfg.Config.Entrypoint)
513 513
 		return "", nil
514 514
 	}
515
-
516
-	req := defaultDispatchReq(b, "abcdef")
517
-	require.NoError(t, from(req))
518
-	b.buildArgs.AddArg("one", strPtr("two"))
519
-
520
-	req.args = []string{"echo foo"}
521
-	require.NoError(t, run(req))
515
+	from := &instructions.Stage{BaseName: "abcdef"}
516
+	err := initializeStage(sb, from)
517
+	require.NoError(t, err)
518
+	sb.state.buildArgs.AddArg("one", strPtr("two"))
519
+	run := &instructions.RunCommand{
520
+		ShellDependantCmdLine: instructions.ShellDependantCmdLine{
521
+			CmdLine:      strslice.StrSlice{"echo foo"},
522
+			PrependShell: true,
523
+		},
524
+	}
525
+	require.NoError(t, dispatch(sb, run))
522 526
 
523 527
 	// Check that runConfig.Cmd has not been modified by run
524
-	assert.Equal(t, origCmd, req.state.runConfig.Cmd)
528
+	assert.Equal(t, origCmd, sb.state.runConfig.Cmd)
525 529
 }
... ...
@@ -4,7 +4,6 @@ package dockerfile
4 4
 
5 5
 import (
6 6
 	"errors"
7
-	"fmt"
8 7
 	"os"
9 8
 	"path/filepath"
10 9
 )
... ...
@@ -23,10 +22,6 @@ func normalizeWorkdir(_ string, current string, requested string) (string, error
23 23
 	return requested, nil
24 24
 }
25 25
 
26
-func errNotJSON(command, _ string) error {
27
-	return fmt.Errorf("%s requires the arguments to be in JSON form", command)
28
-}
29
-
30 26
 // equalEnvKeys compare two strings and returns true if they are equal. On
31 27
 // Windows this comparison is case insensitive.
32 28
 func equalEnvKeys(from, to string) bool {
... ...
@@ -94,25 +94,6 @@ func normalizeWorkdirWindows(current string, requested string) (string, error) {
94 94
 	return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
95 95
 }
96 96
 
97
-func errNotJSON(command, original string) error {
98
-	// For Windows users, give a hint if it looks like it might contain
99
-	// a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"],
100
-	// as JSON must be escaped. Unfortunate...
101
-	//
102
-	// Specifically looking for quote-driveletter-colon-backslash, there's no
103
-	// double backslash and a [] pair. No, this is not perfect, but it doesn't
104
-	// have to be. It's simply a hint to make life a little easier.
105
-	extra := ""
106
-	original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1)))
107
-	if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 &&
108
-		!strings.Contains(original, `\\`) &&
109
-		strings.Contains(original, "[") &&
110
-		strings.Contains(original, "]") {
111
-		extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original)
112
-	}
113
-	return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
114
-}
115
-
116 97
 // equalEnvKeys compare two strings and returns true if they are equal. On
117 98
 // Windows this comparison is case insensitive.
118 99
 func equalEnvKeys(from, to string) bool {
... ...
@@ -20,183 +20,178 @@
20 20
 package dockerfile
21 21
 
22 22
 import (
23
-	"bytes"
24
-	"fmt"
23
+	"reflect"
25 24
 	"runtime"
25
+	"strconv"
26 26
 	"strings"
27 27
 
28 28
 	"github.com/docker/docker/api/types/container"
29 29
 	"github.com/docker/docker/builder"
30
-	"github.com/docker/docker/builder/dockerfile/command"
31
-	"github.com/docker/docker/builder/dockerfile/parser"
30
+	"github.com/docker/docker/builder/dockerfile/instructions"
32 31
 	"github.com/docker/docker/pkg/system"
33 32
 	"github.com/docker/docker/runconfig/opts"
34 33
 	"github.com/pkg/errors"
35 34
 )
36 35
 
37
-// Environment variable interpolation will happen on these statements only.
38
-var replaceEnvAllowed = map[string]bool{
39
-	command.Env:        true,
40
-	command.Label:      true,
41
-	command.Add:        true,
42
-	command.Copy:       true,
43
-	command.Workdir:    true,
44
-	command.Expose:     true,
45
-	command.Volume:     true,
46
-	command.User:       true,
47
-	command.StopSignal: true,
48
-	command.Arg:        true,
49
-}
36
+func dispatch(d dispatchRequest, cmd instructions.Command) error {
37
+	if c, ok := cmd.(instructions.PlatformSpecific); ok {
38
+		err := c.CheckPlatform(d.builder.platform)
39
+		if err != nil {
40
+			return validationError{err}
41
+		}
42
+	}
43
+	runConfigEnv := d.state.runConfig.Env
44
+	envs := append(runConfigEnv, d.state.buildArgs.FilterAllowed(runConfigEnv)...)
50 45
 
51
-// Certain commands are allowed to have their args split into more
52
-// words after env var replacements. Meaning:
53
-//   ENV foo="123 456"
54
-//   EXPOSE $foo
55
-// should result in the same thing as:
56
-//   EXPOSE 123 456
57
-// and not treat "123 456" as a single word.
58
-// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing.
59
-// Quotes will cause it to still be treated as single word.
60
-var allowWordExpansion = map[string]bool{
61
-	command.Expose: true,
62
-}
46
+	if ex, ok := cmd.(instructions.SupportsSingleWordExpansion); ok {
47
+		err := ex.Expand(func(word string) (string, error) {
48
+			return d.shlex.ProcessWord(word, envs)
49
+		})
50
+		if err != nil {
51
+			return validationError{err}
52
+		}
53
+	}
63 54
 
64
-type dispatchRequest struct {
65
-	builder    *Builder // TODO: replace this with a smaller interface
66
-	args       []string
67
-	attributes map[string]bool
68
-	flags      *BFlags
69
-	original   string
70
-	shlex      *ShellLex
71
-	state      *dispatchState
72
-	source     builder.Source
55
+	if d.builder.options.ForceRemove {
56
+		defer d.builder.containerManager.RemoveAll(d.builder.Stdout)
57
+	}
58
+
59
+	switch c := cmd.(type) {
60
+	case *instructions.EnvCommand:
61
+		return dispatchEnv(d, c)
62
+	case *instructions.MaintainerCommand:
63
+		return dispatchMaintainer(d, c)
64
+	case *instructions.LabelCommand:
65
+		return dispatchLabel(d, c)
66
+	case *instructions.AddCommand:
67
+		return dispatchAdd(d, c)
68
+	case *instructions.CopyCommand:
69
+		return dispatchCopy(d, c)
70
+	case *instructions.OnbuildCommand:
71
+		return dispatchOnbuild(d, c)
72
+	case *instructions.WorkdirCommand:
73
+		return dispatchWorkdir(d, c)
74
+	case *instructions.RunCommand:
75
+		return dispatchRun(d, c)
76
+	case *instructions.CmdCommand:
77
+		return dispatchCmd(d, c)
78
+	case *instructions.HealthCheckCommand:
79
+		return dispatchHealthcheck(d, c)
80
+	case *instructions.EntrypointCommand:
81
+		return dispatchEntrypoint(d, c)
82
+	case *instructions.ExposeCommand:
83
+		return dispatchExpose(d, c, envs)
84
+	case *instructions.UserCommand:
85
+		return dispatchUser(d, c)
86
+	case *instructions.VolumeCommand:
87
+		return dispatchVolume(d, c)
88
+	case *instructions.StopSignalCommand:
89
+		return dispatchStopSignal(d, c)
90
+	case *instructions.ArgCommand:
91
+		return dispatchArg(d, c)
92
+	case *instructions.ShellCommand:
93
+		return dispatchShell(d, c)
94
+	}
95
+	return errors.Errorf("unsupported command type: %v", reflect.TypeOf(cmd))
73 96
 }
74 97
 
75
-func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest {
76
-	return dispatchRequest{
77
-		builder:    builder,
78
-		args:       args,
79
-		attributes: options.node.Attributes,
80
-		original:   options.node.Original,
81
-		flags:      NewBFlagsWithArgs(options.node.Flags),
82
-		shlex:      options.shlex,
83
-		state:      options.state,
84
-		source:     options.source,
85
-	}
98
+// dispatchState is a data object which is modified by dispatchers
99
+type dispatchState struct {
100
+	runConfig  *container.Config
101
+	maintainer string
102
+	cmdSet     bool
103
+	imageID    string
104
+	baseImage  builder.Image
105
+	stageName  string
106
+	buildArgs  *buildArgs
86 107
 }
87 108
 
88
-type dispatcher func(dispatchRequest) error
109
+func newDispatchState(baseArgs *buildArgs) *dispatchState {
110
+	args := baseArgs.Clone()
111
+	args.ResetAllowed()
112
+	return &dispatchState{runConfig: &container.Config{}, buildArgs: args}
113
+}
89 114
 
90
-var evaluateTable map[string]dispatcher
115
+type stagesBuildResults struct {
116
+	flat    []*container.Config
117
+	indexed map[string]*container.Config
118
+}
91 119
 
92
-func init() {
93
-	evaluateTable = map[string]dispatcher{
94
-		command.Add:         add,
95
-		command.Arg:         arg,
96
-		command.Cmd:         cmd,
97
-		command.Copy:        dispatchCopy, // copy() is a go builtin
98
-		command.Entrypoint:  entrypoint,
99
-		command.Env:         env,
100
-		command.Expose:      expose,
101
-		command.From:        from,
102
-		command.Healthcheck: healthcheck,
103
-		command.Label:       label,
104
-		command.Maintainer:  maintainer,
105
-		command.Onbuild:     onbuild,
106
-		command.Run:         run,
107
-		command.Shell:       shell,
108
-		command.StopSignal:  stopSignal,
109
-		command.User:        user,
110
-		command.Volume:      volume,
111
-		command.Workdir:     workdir,
120
+func newStagesBuildResults() *stagesBuildResults {
121
+	return &stagesBuildResults{
122
+		indexed: make(map[string]*container.Config),
112 123
 	}
113 124
 }
114 125
 
115
-func formatStep(stepN int, stepTotal int) string {
116
-	return fmt.Sprintf("%d/%d", stepN+1, stepTotal)
126
+func (r *stagesBuildResults) getByName(name string) (*container.Config, bool) {
127
+	c, ok := r.indexed[strings.ToLower(name)]
128
+	return c, ok
117 129
 }
118 130
 
119
-// This method is the entrypoint to all statement handling routines.
120
-//
121
-// Almost all nodes will have this structure:
122
-// Child[Node, Node, Node] where Child is from parser.Node.Children and each
123
-// node comes from parser.Node.Next. This forms a "line" with a statement and
124
-// arguments and we process them in this normalized form by hitting
125
-// evaluateTable with the leaf nodes of the command and the Builder object.
126
-//
127
-// ONBUILD is a special case; in this case the parser will emit:
128
-// Child[Node, Child[Node, Node...]] where the first node is the literal
129
-// "onbuild" and the child entrypoint is the command of the ONBUILD statement,
130
-// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
131
-// deal with that, at least until it becomes more of a general concern with new
132
-// features.
133
-func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
134
-	node := options.node
135
-	cmd := node.Value
136
-	upperCasedCmd := strings.ToUpper(cmd)
137
-
138
-	// To ensure the user is given a decent error message if the platform
139
-	// on which the daemon is running does not support a builder command.
140
-	if err := platformSupports(strings.ToLower(cmd)); err != nil {
141
-		buildsFailed.WithValues(metricsCommandNotSupportedError).Inc()
142
-		return nil, validationError{err}
131
+func (r *stagesBuildResults) validateIndex(i int) error {
132
+	if i == len(r.flat) {
133
+		return errors.New("refers to current build stage")
143 134
 	}
144
-
145
-	msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
146
-		options.stepMsg, upperCasedCmd, formatFlags(node.Flags)))
147
-
148
-	args := []string{}
149
-	ast := node
150
-	if cmd == command.Onbuild {
151
-		var err error
152
-		ast, args, err = handleOnBuildNode(node, msg)
153
-		if err != nil {
154
-			return nil, validationError{err}
155
-		}
135
+	if i < 0 || i > len(r.flat) {
136
+		return errors.New("index out of bounds")
156 137
 	}
138
+	return nil
139
+}
157 140
 
158
-	runConfigEnv := options.state.runConfig.Env
159
-	envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...)
160
-	processFunc := createProcessWordFunc(options.shlex, cmd, envs)
161
-	words, err := getDispatchArgsFromNode(ast, processFunc, msg)
141
+func (r *stagesBuildResults) get(nameOrIndex string) (*container.Config, error) {
142
+	if c, ok := r.getByName(nameOrIndex); ok {
143
+		return c, nil
144
+	}
145
+	ix, err := strconv.ParseInt(nameOrIndex, 10, 0)
162 146
 	if err != nil {
163
-		buildsFailed.WithValues(metricsErrorProcessingCommandsError).Inc()
164
-		return nil, validationError{err}
147
+		return nil, nil
165 148
 	}
166
-	args = append(args, words...)
149
+	if err := r.validateIndex(int(ix)); err != nil {
150
+		return nil, err
151
+	}
152
+	return r.flat[ix], nil
153
+}
167 154
 
168
-	fmt.Fprintln(b.Stdout, msg.String())
155
+func (r *stagesBuildResults) checkStageNameAvailable(name string) error {
156
+	if name != "" {
157
+		if _, ok := r.getByName(name); ok {
158
+			return errors.Errorf("%s stage name already used", name)
159
+		}
160
+	}
161
+	return nil
162
+}
169 163
 
170
-	f, ok := evaluateTable[cmd]
171
-	if !ok {
172
-		buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
173
-		return nil, validationError{errors.Errorf("unknown instruction: %s", upperCasedCmd)}
164
+func (r *stagesBuildResults) commitStage(name string, config *container.Config) error {
165
+	if name != "" {
166
+		if _, ok := r.getByName(name); ok {
167
+			return errors.Errorf("%s stage name already used", name)
168
+		}
169
+		r.indexed[strings.ToLower(name)] = config
174 170
 	}
175
-	options.state.updateRunConfig()
176
-	err = f(newDispatchRequestFromOptions(options, b, args))
177
-	return options.state, err
171
+	r.flat = append(r.flat, config)
172
+	return nil
173
+}
174
+
175
+func commitStage(state *dispatchState, stages *stagesBuildResults) error {
176
+	return stages.commitStage(state.stageName, state.runConfig)
178 177
 }
179 178
 
180
-type dispatchOptions struct {
179
+type dispatchRequest struct {
181 180
 	state   *dispatchState
182
-	stepMsg string
183
-	node    *parser.Node
184 181
 	shlex   *ShellLex
182
+	builder *Builder
185 183
 	source  builder.Source
184
+	stages  *stagesBuildResults
186 185
 }
187 186
 
188
-// dispatchState is a data object which is modified by dispatchers
189
-type dispatchState struct {
190
-	runConfig  *container.Config
191
-	maintainer string
192
-	cmdSet     bool
193
-	imageID    string
194
-	baseImage  builder.Image
195
-	stageName  string
196
-}
197
-
198
-func newDispatchState() *dispatchState {
199
-	return &dispatchState{runConfig: &container.Config{}}
187
+func newDispatchRequest(builder *Builder, escapeToken rune, source builder.Source, buildArgs *buildArgs, stages *stagesBuildResults) dispatchRequest {
188
+	return dispatchRequest{
189
+		state:   newDispatchState(buildArgs),
190
+		shlex:   NewShellLex(escapeToken),
191
+		builder: builder,
192
+		source:  source,
193
+		stages:  stages,
194
+	}
200 195
 }
201 196
 
202 197
 func (s *dispatchState) updateRunConfig() {
... ...
@@ -220,12 +215,14 @@ func (s *dispatchState) beginStage(stageName string, image builder.Image) {
220 220
 	s.imageID = image.ImageID()
221 221
 
222 222
 	if image.RunConfig() != nil {
223
-		s.runConfig = image.RunConfig()
223
+		s.runConfig = copyRunConfig(image.RunConfig()) // copy avoids referencing the same instance when 2 stages have the same base
224 224
 	} else {
225 225
 		s.runConfig = &container.Config{}
226 226
 	}
227 227
 	s.baseImage = image
228 228
 	s.setDefaultPath()
229
+	s.runConfig.OpenStdin = false
230
+	s.runConfig.StdinOnce = false
229 231
 }
230 232
 
231 233
 // Add the default PATH to runConfig.ENV if one exists for the platform and there
... ...
@@ -244,84 +241,3 @@ func (s *dispatchState) setDefaultPath() {
244 244
 		s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv(platform))
245 245
 	}
246 246
 }
247
-
248
-func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
249
-	if ast.Next == nil {
250
-		return nil, nil, validationError{errors.New("ONBUILD requires at least one argument")}
251
-	}
252
-	ast = ast.Next.Children[0]
253
-	msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
254
-	return ast, []string{ast.Value}, nil
255
-}
256
-
257
-func formatFlags(flags []string) string {
258
-	if len(flags) > 0 {
259
-		return " " + strings.Join(flags, " ")
260
-	}
261
-	return ""
262
-}
263
-
264
-func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) {
265
-	args := []string{}
266
-	for i := 0; ast.Next != nil; i++ {
267
-		ast = ast.Next
268
-		words, err := processFunc(ast.Value)
269
-		if err != nil {
270
-			return nil, err
271
-		}
272
-		args = append(args, words...)
273
-		msg.WriteString(" " + ast.Value)
274
-	}
275
-	return args, nil
276
-}
277
-
278
-type processWordFunc func(string) ([]string, error)
279
-
280
-func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
281
-	switch {
282
-	case !replaceEnvAllowed[cmd]:
283
-		return func(word string) ([]string, error) {
284
-			return []string{word}, nil
285
-		}
286
-	case allowWordExpansion[cmd]:
287
-		return func(word string) ([]string, error) {
288
-			return shlex.ProcessWords(word, envs)
289
-		}
290
-	default:
291
-		return func(word string) ([]string, error) {
292
-			word, err := shlex.ProcessWord(word, envs)
293
-			return []string{word}, err
294
-		}
295
-	}
296
-}
297
-
298
-// checkDispatch does a simple check for syntax errors of the Dockerfile.
299
-// Because some of the instructions can only be validated through runtime,
300
-// arg, env, etc., this syntax check will not be complete and could not replace
301
-// the runtime check. Instead, this function is only a helper that allows
302
-// user to find out the obvious error in Dockerfile earlier on.
303
-func checkDispatch(ast *parser.Node) error {
304
-	cmd := ast.Value
305
-	upperCasedCmd := strings.ToUpper(cmd)
306
-
307
-	// To ensure the user is given a decent error message if the platform
308
-	// on which the daemon is running does not support a builder command.
309
-	if err := platformSupports(strings.ToLower(cmd)); err != nil {
310
-		return err
311
-	}
312
-
313
-	// The instruction itself is ONBUILD, we will make sure it follows with at
314
-	// least one argument
315
-	if upperCasedCmd == "ONBUILD" {
316
-		if ast.Next == nil {
317
-			buildsFailed.WithValues(metricsMissingOnbuildArgumentsError).Inc()
318
-			return errors.New("ONBUILD requires at least one argument")
319
-		}
320
-	}
321
-
322
-	if _, ok := evaluateTable[cmd]; ok {
323
-		return nil
324
-	}
325
-	buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
326
-	return errors.Errorf("unknown instruction: %s", upperCasedCmd)
327
-}
... ...
@@ -1,13 +1,9 @@
1 1
 package dockerfile
2 2
 
3 3
 import (
4
-	"io/ioutil"
5
-	"strings"
6 4
 	"testing"
7 5
 
8
-	"github.com/docker/docker/api/types"
9
-	"github.com/docker/docker/api/types/container"
10
-	"github.com/docker/docker/builder/dockerfile/parser"
6
+	"github.com/docker/docker/builder/dockerfile/instructions"
11 7
 	"github.com/docker/docker/builder/remotecontext"
12 8
 	"github.com/docker/docker/internal/testutil"
13 9
 	"github.com/docker/docker/pkg/archive"
... ...
@@ -15,8 +11,9 @@ import (
15 15
 )
16 16
 
17 17
 type dispatchTestCase struct {
18
-	name, dockerfile, expectedError string
19
-	files                           map[string]string
18
+	name, expectedError string
19
+	cmd                 instructions.Command
20
+	files               map[string]string
20 21
 }
21 22
 
22 23
 func init() {
... ...
@@ -24,108 +21,73 @@ func init() {
24 24
 }
25 25
 
26 26
 func initDispatchTestCases() []dispatchTestCase {
27
-	dispatchTestCases := []dispatchTestCase{{
28
-		name: "copyEmptyWhitespace",
29
-		dockerfile: `COPY
30
-	quux \
31
-      bar`,
32
-		expectedError: "COPY requires at least two arguments",
33
-	},
34
-		{
35
-			name:          "ONBUILD forbidden FROM",
36
-			dockerfile:    "ONBUILD FROM scratch",
37
-			expectedError: "FROM isn't allowed as an ONBUILD trigger",
38
-			files:         nil,
39
-		},
40
-		{
41
-			name:          "ONBUILD forbidden MAINTAINER",
42
-			dockerfile:    "ONBUILD MAINTAINER docker.io",
43
-			expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger",
44
-			files:         nil,
45
-		},
46
-		{
47
-			name:          "ARG two arguments",
48
-			dockerfile:    "ARG foo bar",
49
-			expectedError: "ARG requires exactly one argument",
50
-			files:         nil,
51
-		},
52
-		{
53
-			name:          "MAINTAINER unknown flag",
54
-			dockerfile:    "MAINTAINER --boo joe@example.com",
55
-			expectedError: "Unknown flag: boo",
56
-			files:         nil,
57
-		},
58
-		{
59
-			name:          "ADD multiple files to file",
60
-			dockerfile:    "ADD file1.txt file2.txt test",
61
-			expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
62
-			files:         map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
63
-		},
64
-		{
65
-			name:          "JSON ADD multiple files to file",
66
-			dockerfile:    `ADD ["file1.txt", "file2.txt", "test"]`,
67
-			expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
68
-			files:         map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
69
-		},
70
-		{
71
-			name:          "Wildcard ADD multiple files to file",
72
-			dockerfile:    "ADD file*.txt test",
27
+	dispatchTestCases := []dispatchTestCase{
28
+		{
29
+			name: "ADD multiple files to file",
30
+			cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{
31
+				"file1.txt",
32
+				"file2.txt",
33
+				"test",
34
+			}},
73 35
 			expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
74 36
 			files:         map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
75 37
 		},
76 38
 		{
77
-			name:          "Wildcard JSON ADD multiple files to file",
78
-			dockerfile:    `ADD ["file*.txt", "test"]`,
39
+			name: "Wildcard ADD multiple files to file",
40
+			cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{
41
+				"file*.txt",
42
+				"test",
43
+			}},
79 44
 			expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
80 45
 			files:         map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
81 46
 		},
82 47
 		{
83
-			name:          "COPY multiple files to file",
84
-			dockerfile:    "COPY file1.txt file2.txt test",
85
-			expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /",
86
-			files:         map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
87
-		},
88
-		{
89
-			name:          "JSON COPY multiple files to file",
90
-			dockerfile:    `COPY ["file1.txt", "file2.txt", "test"]`,
48
+			name: "COPY multiple files to file",
49
+			cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
50
+				"file1.txt",
51
+				"file2.txt",
52
+				"test",
53
+			}},
91 54
 			expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /",
92 55
 			files:         map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
93 56
 		},
94 57
 		{
95
-			name:          "ADD multiple files to file with whitespace",
96
-			dockerfile:    `ADD [ "test file1.txt", "test file2.txt", "test" ]`,
58
+			name: "ADD multiple files to file with whitespace",
59
+			cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{
60
+				"test file1.txt",
61
+				"test file2.txt",
62
+				"test",
63
+			}},
97 64
 			expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
98 65
 			files:         map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"},
99 66
 		},
100 67
 		{
101
-			name:          "COPY multiple files to file with whitespace",
102
-			dockerfile:    `COPY [ "test file1.txt", "test file2.txt", "test" ]`,
68
+			name: "COPY multiple files to file with whitespace",
69
+			cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
70
+				"test file1.txt",
71
+				"test file2.txt",
72
+				"test",
73
+			}},
103 74
 			expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /",
104 75
 			files:         map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"},
105 76
 		},
106 77
 		{
107
-			name:          "COPY wildcard no files",
108
-			dockerfile:    `COPY file*.txt /tmp/`,
78
+			name: "COPY wildcard no files",
79
+			cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
80
+				"file*.txt",
81
+				"/tmp/",
82
+			}},
109 83
 			expectedError: "COPY failed: no source files were specified",
110 84
 			files:         nil,
111 85
 		},
112 86
 		{
113
-			name:          "COPY url",
114
-			dockerfile:    `COPY https://index.docker.io/robots.txt /`,
87
+			name: "COPY url",
88
+			cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
89
+				"https://index.docker.io/robots.txt",
90
+				"/",
91
+			}},
115 92
 			expectedError: "source can't be a URL for COPY",
116 93
 			files:         nil,
117
-		},
118
-		{
119
-			name:          "Chaining ONBUILD",
120
-			dockerfile:    `ONBUILD ONBUILD RUN touch foobar`,
121
-			expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed",
122
-			files:         nil,
123
-		},
124
-		{
125
-			name:          "Invalid instruction",
126
-			dockerfile:    `foo bar`,
127
-			expectedError: "unknown instruction: FOO",
128
-			files:         nil,
129 94
 		}}
130 95
 
131 96
 	return dispatchTestCases
... ...
@@ -171,33 +133,8 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
171 171
 		}
172 172
 	}()
173 173
 
174
-	r := strings.NewReader(testCase.dockerfile)
175
-	result, err := parser.Parse(r)
176
-
177
-	if err != nil {
178
-		t.Fatalf("Error when parsing Dockerfile: %s", err)
179
-	}
180
-
181
-	options := &types.ImageBuildOptions{
182
-		BuildArgs: make(map[string]*string),
183
-	}
184
-
185
-	b := &Builder{
186
-		options:   options,
187
-		Stdout:    ioutil.Discard,
188
-		buildArgs: newBuildArgs(options.BuildArgs),
189
-	}
190
-
191
-	shlex := NewShellLex(parser.DefaultEscapeToken)
192
-	n := result.AST
193
-	state := &dispatchState{runConfig: &container.Config{}}
194
-	opts := dispatchOptions{
195
-		state:   state,
196
-		stepMsg: formatStep(0, len(n.Children)),
197
-		node:    n.Children[0],
198
-		shlex:   shlex,
199
-		source:  context,
200
-	}
201
-	_, err = b.dispatch(opts)
174
+	b := newBuilderWithMockBackend()
175
+	sb := newDispatchRequest(b, '`', context, newBuildArgs(make(map[string]*string)), newStagesBuildResults())
176
+	err = dispatch(sb, testCase.cmd)
202 177
 	testutil.ErrorContains(t, err, testCase.expectedError)
203 178
 }
204 179
deleted file mode 100644
... ...
@@ -1,9 +0,0 @@
1
-// +build !windows
2
-
3
-package dockerfile
4
-
5
-// platformSupports is a short-term function to give users a quality error
6
-// message if a Dockerfile uses a command not supported on the platform.
7
-func platformSupports(command string) error {
8
-	return nil
9
-}
10 1
deleted file mode 100644
... ...
@@ -1,13 +0,0 @@
1
-package dockerfile
2
-
3
-import "fmt"
4
-
5
-// platformSupports is gives users a quality error message if a Dockerfile uses
6
-// a command not supported on the platform.
7
-func platformSupports(command string) error {
8
-	switch command {
9
-	case "stopsignal":
10
-		return fmt.Errorf("The daemon on this platform does not support the command '%s'", command)
11
-	}
12
-	return nil
13
-}
... ...
@@ -1,9 +1,6 @@
1 1
 package dockerfile
2 2
 
3 3
 import (
4
-	"strconv"
5
-	"strings"
6
-
7 4
 	"github.com/docker/docker/api/types/backend"
8 5
 	"github.com/docker/docker/builder"
9 6
 	"github.com/docker/docker/builder/remotecontext"
... ...
@@ -13,79 +10,6 @@ import (
13 13
 	"golang.org/x/net/context"
14 14
 )
15 15
 
16
-type buildStage struct {
17
-	id string
18
-}
19
-
20
-func newBuildStage(imageID string) *buildStage {
21
-	return &buildStage{id: imageID}
22
-}
23
-
24
-func (b *buildStage) ImageID() string {
25
-	return b.id
26
-}
27
-
28
-func (b *buildStage) update(imageID string) {
29
-	b.id = imageID
30
-}
31
-
32
-// buildStages tracks each stage of a build so they can be retrieved by index
33
-// or by name.
34
-type buildStages struct {
35
-	sequence []*buildStage
36
-	byName   map[string]*buildStage
37
-}
38
-
39
-func newBuildStages() *buildStages {
40
-	return &buildStages{byName: make(map[string]*buildStage)}
41
-}
42
-
43
-func (s *buildStages) getByName(name string) (*buildStage, bool) {
44
-	stage, ok := s.byName[strings.ToLower(name)]
45
-	return stage, ok
46
-}
47
-
48
-func (s *buildStages) get(indexOrName string) (*buildStage, error) {
49
-	index, err := strconv.Atoi(indexOrName)
50
-	if err == nil {
51
-		if err := s.validateIndex(index); err != nil {
52
-			return nil, err
53
-		}
54
-		return s.sequence[index], nil
55
-	}
56
-	if im, ok := s.byName[strings.ToLower(indexOrName)]; ok {
57
-		return im, nil
58
-	}
59
-	return nil, nil
60
-}
61
-
62
-func (s *buildStages) validateIndex(i int) error {
63
-	if i < 0 || i >= len(s.sequence)-1 {
64
-		if i == len(s.sequence)-1 {
65
-			return errors.New("refers to current build stage")
66
-		}
67
-		return errors.New("index out of bounds")
68
-	}
69
-	return nil
70
-}
71
-
72
-func (s *buildStages) add(name string, image builder.Image) error {
73
-	stage := newBuildStage(image.ImageID())
74
-	name = strings.ToLower(name)
75
-	if len(name) > 0 {
76
-		if _, ok := s.byName[name]; ok {
77
-			return errors.Errorf("duplicate name %s", name)
78
-		}
79
-		s.byName[name] = stage
80
-	}
81
-	s.sequence = append(s.sequence, stage)
82
-	return nil
83
-}
84
-
85
-func (s *buildStages) update(imageID string) {
86
-	s.sequence[len(s.sequence)-1].update(imageID)
87
-}
88
-
89 16
 type getAndMountFunc func(string, bool) (builder.Image, builder.ReleaseableLayer, error)
90 17
 
91 18
 // imageSources mounts images and provides a cache for mounted images. It tracks
92 19
new file mode 100644
... ...
@@ -0,0 +1,183 @@
0
+package instructions
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+)
6
+
7
+// FlagType is the type of the build flag
8
+type FlagType int
9
+
10
+const (
11
+	boolType FlagType = iota
12
+	stringType
13
+)
14
+
15
+// BFlags contains all flags information for the builder
16
+type BFlags struct {
17
+	Args  []string // actual flags/args from cmd line
18
+	flags map[string]*Flag
19
+	used  map[string]*Flag
20
+	Err   error
21
+}
22
+
23
+// Flag contains all information for a flag
24
+type Flag struct {
25
+	bf       *BFlags
26
+	name     string
27
+	flagType FlagType
28
+	Value    string
29
+}
30
+
31
+// NewBFlags returns the new BFlags struct
32
+func NewBFlags() *BFlags {
33
+	return &BFlags{
34
+		flags: make(map[string]*Flag),
35
+		used:  make(map[string]*Flag),
36
+	}
37
+}
38
+
39
+// NewBFlagsWithArgs returns the new BFlags struct with Args set to args
40
+func NewBFlagsWithArgs(args []string) *BFlags {
41
+	flags := NewBFlags()
42
+	flags.Args = args
43
+	return flags
44
+}
45
+
46
+// AddBool adds a bool flag to BFlags
47
+// Note, any error will be generated when Parse() is called (see Parse).
48
+func (bf *BFlags) AddBool(name string, def bool) *Flag {
49
+	flag := bf.addFlag(name, boolType)
50
+	if flag == nil {
51
+		return nil
52
+	}
53
+	if def {
54
+		flag.Value = "true"
55
+	} else {
56
+		flag.Value = "false"
57
+	}
58
+	return flag
59
+}
60
+
61
+// AddString adds a string flag to BFlags
62
+// Note, any error will be generated when Parse() is called (see Parse).
63
+func (bf *BFlags) AddString(name string, def string) *Flag {
64
+	flag := bf.addFlag(name, stringType)
65
+	if flag == nil {
66
+		return nil
67
+	}
68
+	flag.Value = def
69
+	return flag
70
+}
71
+
72
+// addFlag is a generic func used by the other AddXXX() func
73
+// to add a new flag to the BFlags struct.
74
+// Note, any error will be generated when Parse() is called (see Parse).
75
+func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag {
76
+	if _, ok := bf.flags[name]; ok {
77
+		bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
78
+		return nil
79
+	}
80
+
81
+	newFlag := &Flag{
82
+		bf:       bf,
83
+		name:     name,
84
+		flagType: flagType,
85
+	}
86
+	bf.flags[name] = newFlag
87
+
88
+	return newFlag
89
+}
90
+
91
+// IsUsed checks if the flag is used
92
+func (fl *Flag) IsUsed() bool {
93
+	if _, ok := fl.bf.used[fl.name]; ok {
94
+		return true
95
+	}
96
+	return false
97
+}
98
+
99
+// IsTrue checks if a bool flag is true
100
+func (fl *Flag) IsTrue() bool {
101
+	if fl.flagType != boolType {
102
+		// Should never get here
103
+		panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
104
+	}
105
+	return fl.Value == "true"
106
+}
107
+
108
+// Parse parses and checks if the BFlags is valid.
109
+// Any error noticed during the AddXXX() funcs will be generated/returned
110
+// here.  We do this because an error during AddXXX() is more like a
111
+// compile time error so it doesn't matter too much when we stop our
112
+// processing as long as we do stop it, so this allows the code
113
+// around AddXXX() to be just:
114
+//     defFlag := AddString("description", "")
115
+// w/o needing to add an if-statement around each one.
116
+func (bf *BFlags) Parse() error {
117
+	// If there was an error while defining the possible flags
118
+	// go ahead and bubble it back up here since we didn't do it
119
+	// earlier in the processing
120
+	if bf.Err != nil {
121
+		return fmt.Errorf("Error setting up flags: %s", bf.Err)
122
+	}
123
+
124
+	for _, arg := range bf.Args {
125
+		if !strings.HasPrefix(arg, "--") {
126
+			return fmt.Errorf("Arg should start with -- : %s", arg)
127
+		}
128
+
129
+		if arg == "--" {
130
+			return nil
131
+		}
132
+
133
+		arg = arg[2:]
134
+		value := ""
135
+
136
+		index := strings.Index(arg, "=")
137
+		if index >= 0 {
138
+			value = arg[index+1:]
139
+			arg = arg[:index]
140
+		}
141
+
142
+		flag, ok := bf.flags[arg]
143
+		if !ok {
144
+			return fmt.Errorf("Unknown flag: %s", arg)
145
+		}
146
+
147
+		if _, ok = bf.used[arg]; ok {
148
+			return fmt.Errorf("Duplicate flag specified: %s", arg)
149
+		}
150
+
151
+		bf.used[arg] = flag
152
+
153
+		switch flag.flagType {
154
+		case boolType:
155
+			// value == "" is only ok if no "=" was specified
156
+			if index >= 0 && value == "" {
157
+				return fmt.Errorf("Missing a value on flag: %s", arg)
158
+			}
159
+
160
+			lower := strings.ToLower(value)
161
+			if lower == "" {
162
+				flag.Value = "true"
163
+			} else if lower == "true" || lower == "false" {
164
+				flag.Value = lower
165
+			} else {
166
+				return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
167
+			}
168
+
169
+		case stringType:
170
+			if index < 0 {
171
+				return fmt.Errorf("Missing a value on flag: %s", arg)
172
+			}
173
+			flag.Value = value
174
+
175
+		default:
176
+			panic("No idea what kind of flag we have! Should never get here!")
177
+		}
178
+
179
+	}
180
+
181
+	return nil
182
+}
0 183
new file mode 100644
... ...
@@ -0,0 +1,187 @@
0
+package instructions
1
+
2
+import (
3
+	"testing"
4
+)
5
+
6
+func TestBuilderFlags(t *testing.T) {
7
+	var expected string
8
+	var err error
9
+
10
+	// ---
11
+
12
+	bf := NewBFlags()
13
+	bf.Args = []string{}
14
+	if err := bf.Parse(); err != nil {
15
+		t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err)
16
+	}
17
+
18
+	// ---
19
+
20
+	bf = NewBFlags()
21
+	bf.Args = []string{"--"}
22
+	if err := bf.Parse(); err != nil {
23
+		t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err)
24
+	}
25
+
26
+	// ---
27
+
28
+	bf = NewBFlags()
29
+	flStr1 := bf.AddString("str1", "")
30
+	flBool1 := bf.AddBool("bool1", false)
31
+	bf.Args = []string{}
32
+	if err = bf.Parse(); err != nil {
33
+		t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err)
34
+	}
35
+
36
+	if flStr1.IsUsed() {
37
+		t.Fatal("Test3 - str1 was not used!")
38
+	}
39
+	if flBool1.IsUsed() {
40
+		t.Fatal("Test3 - bool1 was not used!")
41
+	}
42
+
43
+	// ---
44
+
45
+	bf = NewBFlags()
46
+	flStr1 = bf.AddString("str1", "HI")
47
+	flBool1 = bf.AddBool("bool1", false)
48
+	bf.Args = []string{}
49
+
50
+	if err = bf.Parse(); err != nil {
51
+		t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err)
52
+	}
53
+
54
+	if flStr1.Value != "HI" {
55
+		t.Fatal("Str1 was supposed to default to: HI")
56
+	}
57
+	if flBool1.IsTrue() {
58
+		t.Fatal("Bool1 was supposed to default to: false")
59
+	}
60
+	if flStr1.IsUsed() {
61
+		t.Fatal("Str1 was not used!")
62
+	}
63
+	if flBool1.IsUsed() {
64
+		t.Fatal("Bool1 was not used!")
65
+	}
66
+
67
+	// ---
68
+
69
+	bf = NewBFlags()
70
+	flStr1 = bf.AddString("str1", "HI")
71
+	bf.Args = []string{"--str1"}
72
+
73
+	if err = bf.Parse(); err == nil {
74
+		t.Fatalf("Test %q was supposed to fail", bf.Args)
75
+	}
76
+
77
+	// ---
78
+
79
+	bf = NewBFlags()
80
+	flStr1 = bf.AddString("str1", "HI")
81
+	bf.Args = []string{"--str1="}
82
+
83
+	if err = bf.Parse(); err != nil {
84
+		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
85
+	}
86
+
87
+	expected = ""
88
+	if flStr1.Value != expected {
89
+		t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
90
+	}
91
+
92
+	// ---
93
+
94
+	bf = NewBFlags()
95
+	flStr1 = bf.AddString("str1", "HI")
96
+	bf.Args = []string{"--str1=BYE"}
97
+
98
+	if err = bf.Parse(); err != nil {
99
+		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
100
+	}
101
+
102
+	expected = "BYE"
103
+	if flStr1.Value != expected {
104
+		t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
105
+	}
106
+
107
+	// ---
108
+
109
+	bf = NewBFlags()
110
+	flBool1 = bf.AddBool("bool1", false)
111
+	bf.Args = []string{"--bool1"}
112
+
113
+	if err = bf.Parse(); err != nil {
114
+		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
115
+	}
116
+
117
+	if !flBool1.IsTrue() {
118
+		t.Fatal("Test-b1 Bool1 was supposed to be true")
119
+	}
120
+
121
+	// ---
122
+
123
+	bf = NewBFlags()
124
+	flBool1 = bf.AddBool("bool1", false)
125
+	bf.Args = []string{"--bool1=true"}
126
+
127
+	if err = bf.Parse(); err != nil {
128
+		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
129
+	}
130
+
131
+	if !flBool1.IsTrue() {
132
+		t.Fatal("Test-b2 Bool1 was supposed to be true")
133
+	}
134
+
135
+	// ---
136
+
137
+	bf = NewBFlags()
138
+	flBool1 = bf.AddBool("bool1", false)
139
+	bf.Args = []string{"--bool1=false"}
140
+
141
+	if err = bf.Parse(); err != nil {
142
+		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
143
+	}
144
+
145
+	if flBool1.IsTrue() {
146
+		t.Fatal("Test-b3 Bool1 was supposed to be false")
147
+	}
148
+
149
+	// ---
150
+
151
+	bf = NewBFlags()
152
+	flBool1 = bf.AddBool("bool1", false)
153
+	bf.Args = []string{"--bool1=false1"}
154
+
155
+	if err = bf.Parse(); err == nil {
156
+		t.Fatalf("Test %q was supposed to fail", bf.Args)
157
+	}
158
+
159
+	// ---
160
+
161
+	bf = NewBFlags()
162
+	flBool1 = bf.AddBool("bool1", false)
163
+	bf.Args = []string{"--bool2"}
164
+
165
+	if err = bf.Parse(); err == nil {
166
+		t.Fatalf("Test %q was supposed to fail", bf.Args)
167
+	}
168
+
169
+	// ---
170
+
171
+	bf = NewBFlags()
172
+	flStr1 = bf.AddString("str1", "HI")
173
+	flBool1 = bf.AddBool("bool1", false)
174
+	bf.Args = []string{"--bool1", "--str1=BYE"}
175
+
176
+	if err = bf.Parse(); err != nil {
177
+		t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
178
+	}
179
+
180
+	if flStr1.Value != "BYE" {
181
+		t.Fatalf("Test %s, str1 should be BYE", bf.Args)
182
+	}
183
+	if !flBool1.IsTrue() {
184
+		t.Fatalf("Test %s, bool1 should be true", bf.Args)
185
+	}
186
+}
0 187
new file mode 100644
... ...
@@ -0,0 +1,396 @@
0
+package instructions
1
+
2
+import (
3
+	"errors"
4
+
5
+	"strings"
6
+
7
+	"github.com/docker/docker/api/types/container"
8
+	"github.com/docker/docker/api/types/strslice"
9
+)
10
+
11
+// KeyValuePair represent an arbitrary named value (usefull in slice insted of map[string] string to preserve ordering)
12
+type KeyValuePair struct {
13
+	Key   string
14
+	Value string
15
+}
16
+
17
+func (kvp *KeyValuePair) String() string {
18
+	return kvp.Key + "=" + kvp.Value
19
+}
20
+
21
+// Command is implemented by every command present in a dockerfile
22
+type Command interface {
23
+	Name() string
24
+}
25
+
26
+// KeyValuePairs is a slice of KeyValuePair
27
+type KeyValuePairs []KeyValuePair
28
+
29
+// withNameAndCode is the base of every command in a Dockerfile (String() returns its source code)
30
+type withNameAndCode struct {
31
+	code string
32
+	name string
33
+}
34
+
35
+func (c *withNameAndCode) String() string {
36
+	return c.code
37
+}
38
+
39
+// Name of the command
40
+func (c *withNameAndCode) Name() string {
41
+	return c.name
42
+}
43
+
44
+func newWithNameAndCode(req parseRequest) withNameAndCode {
45
+	return withNameAndCode{code: strings.TrimSpace(req.original), name: req.command}
46
+}
47
+
48
+// SingleWordExpander is a provider for variable expansion where 1 word => 1 output
49
+type SingleWordExpander func(word string) (string, error)
50
+
51
+// SupportsSingleWordExpansion interface marks a command as supporting variable expansion
52
+type SupportsSingleWordExpansion interface {
53
+	Expand(expander SingleWordExpander) error
54
+}
55
+
56
+// PlatformSpecific adds platform checks to a command
57
+type PlatformSpecific interface {
58
+	CheckPlatform(platform string) error
59
+}
60
+
61
+func expandKvp(kvp KeyValuePair, expander SingleWordExpander) (KeyValuePair, error) {
62
+	key, err := expander(kvp.Key)
63
+	if err != nil {
64
+		return KeyValuePair{}, err
65
+	}
66
+	value, err := expander(kvp.Value)
67
+	if err != nil {
68
+		return KeyValuePair{}, err
69
+	}
70
+	return KeyValuePair{Key: key, Value: value}, nil
71
+}
72
+func expandKvpsInPlace(kvps KeyValuePairs, expander SingleWordExpander) error {
73
+	for i, kvp := range kvps {
74
+		newKvp, err := expandKvp(kvp, expander)
75
+		if err != nil {
76
+			return err
77
+		}
78
+		kvps[i] = newKvp
79
+	}
80
+	return nil
81
+}
82
+
83
+func expandSliceInPlace(values []string, expander SingleWordExpander) error {
84
+	for i, v := range values {
85
+		newValue, err := expander(v)
86
+		if err != nil {
87
+			return err
88
+		}
89
+		values[i] = newValue
90
+	}
91
+	return nil
92
+}
93
+
94
+// EnvCommand : ENV key1 value1 [keyN valueN...]
95
+type EnvCommand struct {
96
+	withNameAndCode
97
+	Env KeyValuePairs // kvp slice instead of map to preserve ordering
98
+}
99
+
100
+// Expand variables
101
+func (c *EnvCommand) Expand(expander SingleWordExpander) error {
102
+	return expandKvpsInPlace(c.Env, expander)
103
+}
104
+
105
+// MaintainerCommand : MAINTAINER maintainer_name
106
+type MaintainerCommand struct {
107
+	withNameAndCode
108
+	Maintainer string
109
+}
110
+
111
+// LabelCommand : LABEL some json data describing the image
112
+//
113
+// Sets the Label variable foo to bar,
114
+//
115
+type LabelCommand struct {
116
+	withNameAndCode
117
+	Labels KeyValuePairs // kvp slice instead of map to preserve ordering
118
+}
119
+
120
+// Expand variables
121
+func (c *LabelCommand) Expand(expander SingleWordExpander) error {
122
+	return expandKvpsInPlace(c.Labels, expander)
123
+}
124
+
125
+// SourcesAndDest represent a list of source files and a destination
126
+type SourcesAndDest []string
127
+
128
+// Sources list the source paths
129
+func (s SourcesAndDest) Sources() []string {
130
+	res := make([]string, len(s)-1)
131
+	copy(res, s[:len(s)-1])
132
+	return res
133
+}
134
+
135
+// Dest path of the operation
136
+func (s SourcesAndDest) Dest() string {
137
+	return s[len(s)-1]
138
+}
139
+
140
+// AddCommand : ADD foo /path
141
+//
142
+// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
143
+// exist here. If you do not wish to have this automatic handling, use COPY.
144
+//
145
+type AddCommand struct {
146
+	withNameAndCode
147
+	SourcesAndDest
148
+	Chown string
149
+}
150
+
151
+// Expand variables
152
+func (c *AddCommand) Expand(expander SingleWordExpander) error {
153
+	return expandSliceInPlace(c.SourcesAndDest, expander)
154
+}
155
+
156
+// CopyCommand : COPY foo /path
157
+//
158
+// Same as 'ADD' but without the tar and remote url handling.
159
+//
160
+type CopyCommand struct {
161
+	withNameAndCode
162
+	SourcesAndDest
163
+	From  string
164
+	Chown string
165
+}
166
+
167
+// Expand variables
168
+func (c *CopyCommand) Expand(expander SingleWordExpander) error {
169
+	return expandSliceInPlace(c.SourcesAndDest, expander)
170
+}
171
+
172
+// OnbuildCommand : ONBUILD <some other command>
173
+type OnbuildCommand struct {
174
+	withNameAndCode
175
+	Expression string
176
+}
177
+
178
+// WorkdirCommand : WORKDIR /tmp
179
+//
180
+// Set the working directory for future RUN/CMD/etc statements.
181
+//
182
+type WorkdirCommand struct {
183
+	withNameAndCode
184
+	Path string
185
+}
186
+
187
+// Expand variables
188
+func (c *WorkdirCommand) Expand(expander SingleWordExpander) error {
189
+	p, err := expander(c.Path)
190
+	if err != nil {
191
+		return err
192
+	}
193
+	c.Path = p
194
+	return nil
195
+}
196
+
197
+// ShellDependantCmdLine represents a cmdline optionaly prepended with the shell
198
+type ShellDependantCmdLine struct {
199
+	CmdLine      strslice.StrSlice
200
+	PrependShell bool
201
+}
202
+
203
+// RunCommand : RUN some command yo
204
+//
205
+// run a command and commit the image. Args are automatically prepended with
206
+// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under
207
+// Windows, in the event there is only one argument The difference in processing:
208
+//
209
+// RUN echo hi          # sh -c echo hi       (Linux)
210
+// RUN echo hi          # cmd /S /C echo hi   (Windows)
211
+// RUN [ "echo", "hi" ] # echo hi
212
+//
213
+type RunCommand struct {
214
+	withNameAndCode
215
+	ShellDependantCmdLine
216
+}
217
+
218
+// CmdCommand : CMD foo
219
+//
220
+// Set the default command to run in the container (which may be empty).
221
+// Argument handling is the same as RUN.
222
+//
223
+type CmdCommand struct {
224
+	withNameAndCode
225
+	ShellDependantCmdLine
226
+}
227
+
228
+// HealthCheckCommand : HEALTHCHECK foo
229
+//
230
+// Set the default healthcheck command to run in the container (which may be empty).
231
+// Argument handling is the same as RUN.
232
+//
233
+type HealthCheckCommand struct {
234
+	withNameAndCode
235
+	Health *container.HealthConfig
236
+}
237
+
238
+// EntrypointCommand : ENTRYPOINT /usr/sbin/nginx
239
+//
240
+// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments
241
+// to /usr/sbin/nginx. Uses the default shell if not in JSON format.
242
+//
243
+// Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint
244
+// is initialized at newBuilder time instead of through argument parsing.
245
+//
246
+type EntrypointCommand struct {
247
+	withNameAndCode
248
+	ShellDependantCmdLine
249
+}
250
+
251
+// ExposeCommand : EXPOSE 6667/tcp 7000/tcp
252
+//
253
+// Expose ports for links and port mappings. This all ends up in
254
+// req.runConfig.ExposedPorts for runconfig.
255
+//
256
+type ExposeCommand struct {
257
+	withNameAndCode
258
+	Ports []string
259
+}
260
+
261
+// UserCommand : USER foo
262
+//
263
+// Set the user to 'foo' for future commands and when running the
264
+// ENTRYPOINT/CMD at container run time.
265
+//
266
+type UserCommand struct {
267
+	withNameAndCode
268
+	User string
269
+}
270
+
271
+// Expand variables
272
+func (c *UserCommand) Expand(expander SingleWordExpander) error {
273
+	p, err := expander(c.User)
274
+	if err != nil {
275
+		return err
276
+	}
277
+	c.User = p
278
+	return nil
279
+}
280
+
281
+// VolumeCommand : VOLUME /foo
282
+//
283
+// Expose the volume /foo for use. Will also accept the JSON array form.
284
+//
285
+type VolumeCommand struct {
286
+	withNameAndCode
287
+	Volumes []string
288
+}
289
+
290
+// Expand variables
291
+func (c *VolumeCommand) Expand(expander SingleWordExpander) error {
292
+	return expandSliceInPlace(c.Volumes, expander)
293
+}
294
+
295
+// StopSignalCommand : STOPSIGNAL signal
296
+//
297
+// Set the signal that will be used to kill the container.
298
+type StopSignalCommand struct {
299
+	withNameAndCode
300
+	Signal string
301
+}
302
+
303
+// Expand variables
304
+func (c *StopSignalCommand) Expand(expander SingleWordExpander) error {
305
+	p, err := expander(c.Signal)
306
+	if err != nil {
307
+		return err
308
+	}
309
+	c.Signal = p
310
+	return nil
311
+}
312
+
313
+// CheckPlatform checks that the command is supported in the target platform
314
+func (c *StopSignalCommand) CheckPlatform(platform string) error {
315
+	if platform == "windows" {
316
+		return errors.New("The daemon on this platform does not support the command stopsignal")
317
+	}
318
+	return nil
319
+}
320
+
321
+// ArgCommand : ARG name[=value]
322
+//
323
+// Adds the variable foo to the trusted list of variables that can be passed
324
+// to builder using the --build-arg flag for expansion/substitution or passing to 'run'.
325
+// Dockerfile author may optionally set a default value of this variable.
326
+type ArgCommand struct {
327
+	withNameAndCode
328
+	Key   string
329
+	Value *string
330
+}
331
+
332
+// Expand variables
333
+func (c *ArgCommand) Expand(expander SingleWordExpander) error {
334
+	p, err := expander(c.Key)
335
+	if err != nil {
336
+		return err
337
+	}
338
+	c.Key = p
339
+	if c.Value != nil {
340
+		p, err = expander(*c.Value)
341
+		if err != nil {
342
+			return err
343
+		}
344
+		c.Value = &p
345
+	}
346
+	return nil
347
+}
348
+
349
+// ShellCommand : SHELL powershell -command
350
+//
351
+// Set the non-default shell to use.
352
+type ShellCommand struct {
353
+	withNameAndCode
354
+	Shell strslice.StrSlice
355
+}
356
+
357
+// Stage represents a single stage in a multi-stage build
358
+type Stage struct {
359
+	Name       string
360
+	Commands   []Command
361
+	BaseName   string
362
+	SourceCode string
363
+}
364
+
365
+// AddCommand to the stage
366
+func (s *Stage) AddCommand(cmd Command) {
367
+	// todo: validate cmd type
368
+	s.Commands = append(s.Commands, cmd)
369
+}
370
+
371
+// IsCurrentStage check if the stage name is the current stage
372
+func IsCurrentStage(s []Stage, name string) bool {
373
+	if len(s) == 0 {
374
+		return false
375
+	}
376
+	return s[len(s)-1].Name == name
377
+}
378
+
379
+// CurrentStage return the last stage in a slice
380
+func CurrentStage(s []Stage) (*Stage, error) {
381
+	if len(s) == 0 {
382
+		return nil, errors.New("No build stage in current context")
383
+	}
384
+	return &s[len(s)-1], nil
385
+}
386
+
387
+// HasStage looks for the presence of a given stage name
388
+func HasStage(s []Stage, name string) (int, bool) {
389
+	for i, stage := range s {
390
+		if stage.Name == name {
391
+			return i, true
392
+		}
393
+	}
394
+	return -1, false
395
+}
0 396
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+// +build !windows
1
+
2
+package instructions
3
+
4
+import "fmt"
5
+
6
+func errNotJSON(command, _ string) error {
7
+	return fmt.Errorf("%s requires the arguments to be in JSON form", command)
8
+}
0 9
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+package instructions
1
+
2
+import (
3
+	"fmt"
4
+	"path/filepath"
5
+	"regexp"
6
+	"strings"
7
+)
8
+
9
+func errNotJSON(command, original string) error {
10
+	// For Windows users, give a hint if it looks like it might contain
11
+	// a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"],
12
+	// as JSON must be escaped. Unfortunate...
13
+	//
14
+	// Specifically looking for quote-driveletter-colon-backslash, there's no
15
+	// double backslash and a [] pair. No, this is not perfect, but it doesn't
16
+	// have to be. It's simply a hint to make life a little easier.
17
+	extra := ""
18
+	original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1)))
19
+	if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 &&
20
+		!strings.Contains(original, `\\`) &&
21
+		strings.Contains(original, "[") &&
22
+		strings.Contains(original, "]") {
23
+		extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original)
24
+	}
25
+	return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
26
+}
0 27
new file mode 100644
... ...
@@ -0,0 +1,635 @@
0
+package instructions
1
+
2
+import (
3
+	"fmt"
4
+	"regexp"
5
+	"sort"
6
+	"strconv"
7
+	"strings"
8
+	"time"
9
+
10
+	"github.com/docker/docker/api/types/container"
11
+	"github.com/docker/docker/api/types/strslice"
12
+	"github.com/docker/docker/builder/dockerfile/command"
13
+	"github.com/docker/docker/builder/dockerfile/parser"
14
+	"github.com/pkg/errors"
15
+)
16
+
17
+type parseRequest struct {
18
+	command    string
19
+	args       []string
20
+	attributes map[string]bool
21
+	flags      *BFlags
22
+	original   string
23
+}
24
+
25
+func nodeArgs(node *parser.Node) []string {
26
+	result := []string{}
27
+	for ; node.Next != nil; node = node.Next {
28
+		arg := node.Next
29
+		if len(arg.Children) == 0 {
30
+			result = append(result, arg.Value)
31
+		} else if len(arg.Children) == 1 {
32
+			//sub command
33
+			result = append(result, arg.Children[0].Value)
34
+			result = append(result, nodeArgs(arg.Children[0])...)
35
+		}
36
+	}
37
+	return result
38
+}
39
+
40
+func newParseRequestFromNode(node *parser.Node) parseRequest {
41
+	return parseRequest{
42
+		command:    node.Value,
43
+		args:       nodeArgs(node),
44
+		attributes: node.Attributes,
45
+		original:   node.Original,
46
+		flags:      NewBFlagsWithArgs(node.Flags),
47
+	}
48
+}
49
+
50
+// ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement)
51
+func ParseInstruction(node *parser.Node) (interface{}, error) {
52
+	req := newParseRequestFromNode(node)
53
+	switch node.Value {
54
+	case command.Env:
55
+		return parseEnv(req)
56
+	case command.Maintainer:
57
+		return parseMaintainer(req)
58
+	case command.Label:
59
+		return parseLabel(req)
60
+	case command.Add:
61
+		return parseAdd(req)
62
+	case command.Copy:
63
+		return parseCopy(req)
64
+	case command.From:
65
+		return parseFrom(req)
66
+	case command.Onbuild:
67
+		return parseOnBuild(req)
68
+	case command.Workdir:
69
+		return parseWorkdir(req)
70
+	case command.Run:
71
+		return parseRun(req)
72
+	case command.Cmd:
73
+		return parseCmd(req)
74
+	case command.Healthcheck:
75
+		return parseHealthcheck(req)
76
+	case command.Entrypoint:
77
+		return parseEntrypoint(req)
78
+	case command.Expose:
79
+		return parseExpose(req)
80
+	case command.User:
81
+		return parseUser(req)
82
+	case command.Volume:
83
+		return parseVolume(req)
84
+	case command.StopSignal:
85
+		return parseStopSignal(req)
86
+	case command.Arg:
87
+		return parseArg(req)
88
+	case command.Shell:
89
+		return parseShell(req)
90
+	}
91
+
92
+	return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine}
93
+}
94
+
95
+// ParseCommand converts an AST to a typed Command
96
+func ParseCommand(node *parser.Node) (Command, error) {
97
+	s, err := ParseInstruction(node)
98
+	if err != nil {
99
+		return nil, err
100
+	}
101
+	if c, ok := s.(Command); ok {
102
+		return c, nil
103
+	}
104
+	return nil, errors.Errorf("%T is not a command type", s)
105
+}
106
+
107
+// UnknownInstruction represents an error occuring when a command is unresolvable
108
+type UnknownInstruction struct {
109
+	Line        int
110
+	Instruction string
111
+}
112
+
113
+func (e *UnknownInstruction) Error() string {
114
+	return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction))
115
+}
116
+
117
+// IsUnknownInstruction checks if the error is an UnknownInstruction or a parseError containing an UnknownInstruction
118
+func IsUnknownInstruction(err error) bool {
119
+	_, ok := err.(*UnknownInstruction)
120
+	if !ok {
121
+		var pe *parseError
122
+		if pe, ok = err.(*parseError); ok {
123
+			_, ok = pe.inner.(*UnknownInstruction)
124
+		}
125
+	}
126
+	return ok
127
+}
128
+
129
+type parseError struct {
130
+	inner error
131
+	node  *parser.Node
132
+}
133
+
134
+func (e *parseError) Error() string {
135
+	return fmt.Sprintf("Dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error())
136
+}
137
+
138
+// Parse a docker file into a collection of buildable stages
139
+func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) {
140
+	for _, n := range ast.Children {
141
+		cmd, err := ParseInstruction(n)
142
+		if err != nil {
143
+			return nil, nil, &parseError{inner: err, node: n}
144
+		}
145
+		if len(stages) == 0 {
146
+			// meta arg case
147
+			if a, isArg := cmd.(*ArgCommand); isArg {
148
+				metaArgs = append(metaArgs, *a)
149
+				continue
150
+			}
151
+		}
152
+		switch c := cmd.(type) {
153
+		case *Stage:
154
+			stages = append(stages, *c)
155
+		case Command:
156
+			stage, err := CurrentStage(stages)
157
+			if err != nil {
158
+				return nil, nil, err
159
+			}
160
+			stage.AddCommand(c)
161
+		default:
162
+			return nil, nil, errors.Errorf("%T is not a command type", cmd)
163
+		}
164
+
165
+	}
166
+	return stages, metaArgs, nil
167
+}
168
+
169
+func parseKvps(args []string, cmdName string) (KeyValuePairs, error) {
170
+	if len(args) == 0 {
171
+		return nil, errAtLeastOneArgument(cmdName)
172
+	}
173
+	if len(args)%2 != 0 {
174
+		// should never get here, but just in case
175
+		return nil, errTooManyArguments(cmdName)
176
+	}
177
+	var res KeyValuePairs
178
+	for j := 0; j < len(args); j += 2 {
179
+		if len(args[j]) == 0 {
180
+			return nil, errBlankCommandNames(cmdName)
181
+		}
182
+		name := args[j]
183
+		value := args[j+1]
184
+		res = append(res, KeyValuePair{Key: name, Value: value})
185
+	}
186
+	return res, nil
187
+}
188
+
189
+func parseEnv(req parseRequest) (*EnvCommand, error) {
190
+
191
+	if err := req.flags.Parse(); err != nil {
192
+		return nil, err
193
+	}
194
+	envs, err := parseKvps(req.args, "ENV")
195
+	if err != nil {
196
+		return nil, err
197
+	}
198
+	return &EnvCommand{
199
+		Env:             envs,
200
+		withNameAndCode: newWithNameAndCode(req),
201
+	}, nil
202
+}
203
+
204
+func parseMaintainer(req parseRequest) (*MaintainerCommand, error) {
205
+	if len(req.args) != 1 {
206
+		return nil, errExactlyOneArgument("MAINTAINER")
207
+	}
208
+
209
+	if err := req.flags.Parse(); err != nil {
210
+		return nil, err
211
+	}
212
+	return &MaintainerCommand{
213
+		Maintainer:      req.args[0],
214
+		withNameAndCode: newWithNameAndCode(req),
215
+	}, nil
216
+}
217
+
218
+func parseLabel(req parseRequest) (*LabelCommand, error) {
219
+
220
+	if err := req.flags.Parse(); err != nil {
221
+		return nil, err
222
+	}
223
+
224
+	labels, err := parseKvps(req.args, "LABEL")
225
+	if err != nil {
226
+		return nil, err
227
+	}
228
+
229
+	return &LabelCommand{
230
+		Labels:          labels,
231
+		withNameAndCode: newWithNameAndCode(req),
232
+	}, nil
233
+}
234
+
235
+func parseAdd(req parseRequest) (*AddCommand, error) {
236
+	if len(req.args) < 2 {
237
+		return nil, errAtLeastTwoArguments("ADD")
238
+	}
239
+	flChown := req.flags.AddString("chown", "")
240
+	if err := req.flags.Parse(); err != nil {
241
+		return nil, err
242
+	}
243
+	return &AddCommand{
244
+		SourcesAndDest:  SourcesAndDest(req.args),
245
+		withNameAndCode: newWithNameAndCode(req),
246
+		Chown:           flChown.Value,
247
+	}, nil
248
+}
249
+
250
+func parseCopy(req parseRequest) (*CopyCommand, error) {
251
+	if len(req.args) < 2 {
252
+		return nil, errAtLeastTwoArguments("COPY")
253
+	}
254
+	flChown := req.flags.AddString("chown", "")
255
+	flFrom := req.flags.AddString("from", "")
256
+	if err := req.flags.Parse(); err != nil {
257
+		return nil, err
258
+	}
259
+	return &CopyCommand{
260
+		SourcesAndDest:  SourcesAndDest(req.args),
261
+		From:            flFrom.Value,
262
+		withNameAndCode: newWithNameAndCode(req),
263
+		Chown:           flChown.Value,
264
+	}, nil
265
+}
266
+
267
+func parseFrom(req parseRequest) (*Stage, error) {
268
+	stageName, err := parseBuildStageName(req.args)
269
+	if err != nil {
270
+		return nil, err
271
+	}
272
+
273
+	if err := req.flags.Parse(); err != nil {
274
+		return nil, err
275
+	}
276
+	code := strings.TrimSpace(req.original)
277
+
278
+	return &Stage{
279
+		BaseName:   req.args[0],
280
+		Name:       stageName,
281
+		SourceCode: code,
282
+		Commands:   []Command{},
283
+	}, nil
284
+
285
+}
286
+
287
+func parseBuildStageName(args []string) (string, error) {
288
+	stageName := ""
289
+	switch {
290
+	case len(args) == 3 && strings.EqualFold(args[1], "as"):
291
+		stageName = strings.ToLower(args[2])
292
+		if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok {
293
+			return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName)
294
+		}
295
+	case len(args) != 1:
296
+		return "", errors.New("FROM requires either one or three arguments")
297
+	}
298
+
299
+	return stageName, nil
300
+}
301
+
302
+func parseOnBuild(req parseRequest) (*OnbuildCommand, error) {
303
+	if len(req.args) == 0 {
304
+		return nil, errAtLeastOneArgument("ONBUILD")
305
+	}
306
+	if err := req.flags.Parse(); err != nil {
307
+		return nil, err
308
+	}
309
+
310
+	triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0]))
311
+	switch strings.ToUpper(triggerInstruction) {
312
+	case "ONBUILD":
313
+		return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
314
+	case "MAINTAINER", "FROM":
315
+		return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
316
+	}
317
+
318
+	original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
319
+	return &OnbuildCommand{
320
+		Expression:      original,
321
+		withNameAndCode: newWithNameAndCode(req),
322
+	}, nil
323
+
324
+}
325
+
326
+func parseWorkdir(req parseRequest) (*WorkdirCommand, error) {
327
+	if len(req.args) != 1 {
328
+		return nil, errExactlyOneArgument("WORKDIR")
329
+	}
330
+
331
+	err := req.flags.Parse()
332
+	if err != nil {
333
+		return nil, err
334
+	}
335
+	return &WorkdirCommand{
336
+		Path:            req.args[0],
337
+		withNameAndCode: newWithNameAndCode(req),
338
+	}, nil
339
+
340
+}
341
+
342
+func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine {
343
+	args := handleJSONArgs(req.args, req.attributes)
344
+	cmd := strslice.StrSlice(args)
345
+	if emptyAsNil && len(cmd) == 0 {
346
+		cmd = nil
347
+	}
348
+	return ShellDependantCmdLine{
349
+		CmdLine:      cmd,
350
+		PrependShell: !req.attributes["json"],
351
+	}
352
+}
353
+
354
+func parseRun(req parseRequest) (*RunCommand, error) {
355
+
356
+	if err := req.flags.Parse(); err != nil {
357
+		return nil, err
358
+	}
359
+	return &RunCommand{
360
+		ShellDependantCmdLine: parseShellDependentCommand(req, false),
361
+		withNameAndCode:       newWithNameAndCode(req),
362
+	}, nil
363
+
364
+}
365
+
366
+func parseCmd(req parseRequest) (*CmdCommand, error) {
367
+	if err := req.flags.Parse(); err != nil {
368
+		return nil, err
369
+	}
370
+	return &CmdCommand{
371
+		ShellDependantCmdLine: parseShellDependentCommand(req, false),
372
+		withNameAndCode:       newWithNameAndCode(req),
373
+	}, nil
374
+
375
+}
376
+
377
+func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) {
378
+	if err := req.flags.Parse(); err != nil {
379
+		return nil, err
380
+	}
381
+
382
+	cmd := &EntrypointCommand{
383
+		ShellDependantCmdLine: parseShellDependentCommand(req, true),
384
+		withNameAndCode:       newWithNameAndCode(req),
385
+	}
386
+
387
+	return cmd, nil
388
+}
389
+
390
+// parseOptInterval(flag) is the duration of flag.Value, or 0 if
391
+// empty. An error is reported if the value is given and less than minimum duration.
392
+func parseOptInterval(f *Flag) (time.Duration, error) {
393
+	s := f.Value
394
+	if s == "" {
395
+		return 0, nil
396
+	}
397
+	d, err := time.ParseDuration(s)
398
+	if err != nil {
399
+		return 0, err
400
+	}
401
+	if d < container.MinimumDuration {
402
+		return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration)
403
+	}
404
+	return d, nil
405
+}
406
+func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) {
407
+	if len(req.args) == 0 {
408
+		return nil, errAtLeastOneArgument("HEALTHCHECK")
409
+	}
410
+	cmd := &HealthCheckCommand{
411
+		withNameAndCode: newWithNameAndCode(req),
412
+	}
413
+
414
+	typ := strings.ToUpper(req.args[0])
415
+	args := req.args[1:]
416
+	if typ == "NONE" {
417
+		if len(args) != 0 {
418
+			return nil, errors.New("HEALTHCHECK NONE takes no arguments")
419
+		}
420
+		test := strslice.StrSlice{typ}
421
+		cmd.Health = &container.HealthConfig{
422
+			Test: test,
423
+		}
424
+	} else {
425
+
426
+		healthcheck := container.HealthConfig{}
427
+
428
+		flInterval := req.flags.AddString("interval", "")
429
+		flTimeout := req.flags.AddString("timeout", "")
430
+		flStartPeriod := req.flags.AddString("start-period", "")
431
+		flRetries := req.flags.AddString("retries", "")
432
+
433
+		if err := req.flags.Parse(); err != nil {
434
+			return nil, err
435
+		}
436
+
437
+		switch typ {
438
+		case "CMD":
439
+			cmdSlice := handleJSONArgs(args, req.attributes)
440
+			if len(cmdSlice) == 0 {
441
+				return nil, errors.New("Missing command after HEALTHCHECK CMD")
442
+			}
443
+
444
+			if !req.attributes["json"] {
445
+				typ = "CMD-SHELL"
446
+			}
447
+
448
+			healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...))
449
+		default:
450
+			return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ)
451
+		}
452
+
453
+		interval, err := parseOptInterval(flInterval)
454
+		if err != nil {
455
+			return nil, err
456
+		}
457
+		healthcheck.Interval = interval
458
+
459
+		timeout, err := parseOptInterval(flTimeout)
460
+		if err != nil {
461
+			return nil, err
462
+		}
463
+		healthcheck.Timeout = timeout
464
+
465
+		startPeriod, err := parseOptInterval(flStartPeriod)
466
+		if err != nil {
467
+			return nil, err
468
+		}
469
+		healthcheck.StartPeriod = startPeriod
470
+
471
+		if flRetries.Value != "" {
472
+			retries, err := strconv.ParseInt(flRetries.Value, 10, 32)
473
+			if err != nil {
474
+				return nil, err
475
+			}
476
+			if retries < 1 {
477
+				return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries)
478
+			}
479
+			healthcheck.Retries = int(retries)
480
+		} else {
481
+			healthcheck.Retries = 0
482
+		}
483
+
484
+		cmd.Health = &healthcheck
485
+	}
486
+	return cmd, nil
487
+}
488
+
489
+func parseExpose(req parseRequest) (*ExposeCommand, error) {
490
+	portsTab := req.args
491
+
492
+	if len(req.args) == 0 {
493
+		return nil, errAtLeastOneArgument("EXPOSE")
494
+	}
495
+
496
+	if err := req.flags.Parse(); err != nil {
497
+		return nil, err
498
+	}
499
+
500
+	sort.Strings(portsTab)
501
+	return &ExposeCommand{
502
+		Ports:           portsTab,
503
+		withNameAndCode: newWithNameAndCode(req),
504
+	}, nil
505
+}
506
+
507
+func parseUser(req parseRequest) (*UserCommand, error) {
508
+	if len(req.args) != 1 {
509
+		return nil, errExactlyOneArgument("USER")
510
+	}
511
+
512
+	if err := req.flags.Parse(); err != nil {
513
+		return nil, err
514
+	}
515
+	return &UserCommand{
516
+		User:            req.args[0],
517
+		withNameAndCode: newWithNameAndCode(req),
518
+	}, nil
519
+}
520
+
521
+func parseVolume(req parseRequest) (*VolumeCommand, error) {
522
+	if len(req.args) == 0 {
523
+		return nil, errAtLeastOneArgument("VOLUME")
524
+	}
525
+
526
+	if err := req.flags.Parse(); err != nil {
527
+		return nil, err
528
+	}
529
+
530
+	cmd := &VolumeCommand{
531
+		withNameAndCode: newWithNameAndCode(req),
532
+	}
533
+
534
+	for _, v := range req.args {
535
+		v = strings.TrimSpace(v)
536
+		if v == "" {
537
+			return nil, errors.New("VOLUME specified can not be an empty string")
538
+		}
539
+		cmd.Volumes = append(cmd.Volumes, v)
540
+	}
541
+	return cmd, nil
542
+
543
+}
544
+
545
+func parseStopSignal(req parseRequest) (*StopSignalCommand, error) {
546
+	if len(req.args) != 1 {
547
+		return nil, errExactlyOneArgument("STOPSIGNAL")
548
+	}
549
+	sig := req.args[0]
550
+
551
+	cmd := &StopSignalCommand{
552
+		Signal:          sig,
553
+		withNameAndCode: newWithNameAndCode(req),
554
+	}
555
+	return cmd, nil
556
+
557
+}
558
+
559
+func parseArg(req parseRequest) (*ArgCommand, error) {
560
+	if len(req.args) != 1 {
561
+		return nil, errExactlyOneArgument("ARG")
562
+	}
563
+
564
+	var (
565
+		name     string
566
+		newValue *string
567
+	)
568
+
569
+	arg := req.args[0]
570
+	// 'arg' can just be a name or name-value pair. Note that this is different
571
+	// from 'env' that handles the split of name and value at the parser level.
572
+	// The reason for doing it differently for 'arg' is that we support just
573
+	// defining an arg and not assign it a value (while 'env' always expects a
574
+	// name-value pair). If possible, it will be good to harmonize the two.
575
+	if strings.Contains(arg, "=") {
576
+		parts := strings.SplitN(arg, "=", 2)
577
+		if len(parts[0]) == 0 {
578
+			return nil, errBlankCommandNames("ARG")
579
+		}
580
+
581
+		name = parts[0]
582
+		newValue = &parts[1]
583
+	} else {
584
+		name = arg
585
+	}
586
+
587
+	return &ArgCommand{
588
+		Key:             name,
589
+		Value:           newValue,
590
+		withNameAndCode: newWithNameAndCode(req),
591
+	}, nil
592
+}
593
+
594
+func parseShell(req parseRequest) (*ShellCommand, error) {
595
+	if err := req.flags.Parse(); err != nil {
596
+		return nil, err
597
+	}
598
+	shellSlice := handleJSONArgs(req.args, req.attributes)
599
+	switch {
600
+	case len(shellSlice) == 0:
601
+		// SHELL []
602
+		return nil, errAtLeastOneArgument("SHELL")
603
+	case req.attributes["json"]:
604
+		// SHELL ["powershell", "-command"]
605
+
606
+		return &ShellCommand{
607
+			Shell:           strslice.StrSlice(shellSlice),
608
+			withNameAndCode: newWithNameAndCode(req),
609
+		}, nil
610
+	default:
611
+		// SHELL powershell -command - not JSON
612
+		return nil, errNotJSON("SHELL", req.original)
613
+	}
614
+}
615
+
616
+func errAtLeastOneArgument(command string) error {
617
+	return errors.Errorf("%s requires at least one argument", command)
618
+}
619
+
620
+func errExactlyOneArgument(command string) error {
621
+	return errors.Errorf("%s requires exactly one argument", command)
622
+}
623
+
624
+func errAtLeastTwoArguments(command string) error {
625
+	return errors.Errorf("%s requires at least two arguments", command)
626
+}
627
+
628
+func errBlankCommandNames(command string) error {
629
+	return errors.Errorf("%s names can not be blank", command)
630
+}
631
+
632
+func errTooManyArguments(command string) error {
633
+	return errors.Errorf("Bad input to %s, too many arguments", command)
634
+}
0 635
new file mode 100644
... ...
@@ -0,0 +1,204 @@
0
+package instructions
1
+
2
+import (
3
+	"strings"
4
+	"testing"
5
+
6
+	"github.com/docker/docker/builder/dockerfile/command"
7
+	"github.com/docker/docker/builder/dockerfile/parser"
8
+	"github.com/docker/docker/internal/testutil"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/require"
11
+)
12
+
13
+func TestCommandsExactlyOneArgument(t *testing.T) {
14
+	commands := []string{
15
+		"MAINTAINER",
16
+		"WORKDIR",
17
+		"USER",
18
+		"STOPSIGNAL",
19
+	}
20
+
21
+	for _, command := range commands {
22
+		ast, err := parser.Parse(strings.NewReader(command))
23
+		require.NoError(t, err)
24
+		_, err = ParseInstruction(ast.AST.Children[0])
25
+		assert.EqualError(t, err, errExactlyOneArgument(command).Error())
26
+	}
27
+}
28
+
29
+func TestCommandsAtLeastOneArgument(t *testing.T) {
30
+	commands := []string{
31
+		"ENV",
32
+		"LABEL",
33
+		"ONBUILD",
34
+		"HEALTHCHECK",
35
+		"EXPOSE",
36
+		"VOLUME",
37
+	}
38
+
39
+	for _, command := range commands {
40
+		ast, err := parser.Parse(strings.NewReader(command))
41
+		require.NoError(t, err)
42
+		_, err = ParseInstruction(ast.AST.Children[0])
43
+		assert.EqualError(t, err, errAtLeastOneArgument(command).Error())
44
+	}
45
+}
46
+
47
+func TestCommandsAtLeastTwoArgument(t *testing.T) {
48
+	commands := []string{
49
+		"ADD",
50
+		"COPY",
51
+	}
52
+
53
+	for _, command := range commands {
54
+		ast, err := parser.Parse(strings.NewReader(command + " arg1"))
55
+		require.NoError(t, err)
56
+		_, err = ParseInstruction(ast.AST.Children[0])
57
+		assert.EqualError(t, err, errAtLeastTwoArguments(command).Error())
58
+	}
59
+}
60
+
61
+func TestCommandsTooManyArguments(t *testing.T) {
62
+	commands := []string{
63
+		"ENV",
64
+		"LABEL",
65
+	}
66
+
67
+	for _, command := range commands {
68
+		node := &parser.Node{
69
+			Original: command + "arg1 arg2 arg3",
70
+			Value:    strings.ToLower(command),
71
+			Next: &parser.Node{
72
+				Value: "arg1",
73
+				Next: &parser.Node{
74
+					Value: "arg2",
75
+					Next: &parser.Node{
76
+						Value: "arg3",
77
+					},
78
+				},
79
+			},
80
+		}
81
+		_, err := ParseInstruction(node)
82
+		assert.EqualError(t, err, errTooManyArguments(command).Error())
83
+	}
84
+}
85
+
86
+func TestCommandsBlankNames(t *testing.T) {
87
+	commands := []string{
88
+		"ENV",
89
+		"LABEL",
90
+	}
91
+
92
+	for _, command := range commands {
93
+		node := &parser.Node{
94
+			Original: command + " =arg2",
95
+			Value:    strings.ToLower(command),
96
+			Next: &parser.Node{
97
+				Value: "",
98
+				Next: &parser.Node{
99
+					Value: "arg2",
100
+				},
101
+			},
102
+		}
103
+		_, err := ParseInstruction(node)
104
+		assert.EqualError(t, err, errBlankCommandNames(command).Error())
105
+	}
106
+}
107
+
108
+func TestHealthCheckCmd(t *testing.T) {
109
+	node := &parser.Node{
110
+		Value: command.Healthcheck,
111
+		Next: &parser.Node{
112
+			Value: "CMD",
113
+			Next: &parser.Node{
114
+				Value: "hello",
115
+				Next: &parser.Node{
116
+					Value: "world",
117
+				},
118
+			},
119
+		},
120
+	}
121
+	cmd, err := ParseInstruction(node)
122
+	assert.NoError(t, err)
123
+	hc, ok := cmd.(*HealthCheckCommand)
124
+	assert.True(t, ok)
125
+	expected := []string{"CMD-SHELL", "hello world"}
126
+	assert.Equal(t, expected, hc.Health.Test)
127
+}
128
+
129
+func TestParseOptInterval(t *testing.T) {
130
+	flInterval := &Flag{
131
+		name:     "interval",
132
+		flagType: stringType,
133
+		Value:    "50ns",
134
+	}
135
+	_, err := parseOptInterval(flInterval)
136
+	testutil.ErrorContains(t, err, "cannot be less than 1ms")
137
+
138
+	flInterval.Value = "1ms"
139
+	_, err = parseOptInterval(flInterval)
140
+	require.NoError(t, err)
141
+}
142
+
143
+func TestErrorCases(t *testing.T) {
144
+	cases := []struct {
145
+		name          string
146
+		dockerfile    string
147
+		expectedError string
148
+	}{
149
+		{
150
+			name: "copyEmptyWhitespace",
151
+			dockerfile: `COPY	
152
+		quux \
153
+      bar`,
154
+			expectedError: "COPY requires at least two arguments",
155
+		},
156
+		{
157
+			name:          "ONBUILD forbidden FROM",
158
+			dockerfile:    "ONBUILD FROM scratch",
159
+			expectedError: "FROM isn't allowed as an ONBUILD trigger",
160
+		},
161
+		{
162
+			name:          "ONBUILD forbidden MAINTAINER",
163
+			dockerfile:    "ONBUILD MAINTAINER docker.io",
164
+			expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger",
165
+		},
166
+		{
167
+			name:          "ARG two arguments",
168
+			dockerfile:    "ARG foo bar",
169
+			expectedError: "ARG requires exactly one argument",
170
+		},
171
+		{
172
+			name:          "MAINTAINER unknown flag",
173
+			dockerfile:    "MAINTAINER --boo joe@example.com",
174
+			expectedError: "Unknown flag: boo",
175
+		},
176
+		{
177
+			name:          "Chaining ONBUILD",
178
+			dockerfile:    `ONBUILD ONBUILD RUN touch foobar`,
179
+			expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed",
180
+		},
181
+		{
182
+			name:          "Invalid instruction",
183
+			dockerfile:    `foo bar`,
184
+			expectedError: "unknown instruction: FOO",
185
+		},
186
+	}
187
+	for _, c := range cases {
188
+		r := strings.NewReader(c.dockerfile)
189
+		ast, err := parser.Parse(r)
190
+
191
+		if err != nil {
192
+			t.Fatalf("Error when parsing Dockerfile: %s", err)
193
+		}
194
+		n := ast.AST.Children[0]
195
+		_, err = ParseInstruction(n)
196
+		if err != nil {
197
+			testutil.ErrorContains(t, err, c.expectedError)
198
+			return
199
+		}
200
+		t.Fatalf("No error when executing test %s", c.name)
201
+	}
202
+
203
+}
0 204
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+package instructions
1
+
2
+import "strings"
3
+
4
+// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile
5
+// for exec form it returns untouched args slice
6
+// for shell form it returns concatenated args as the first element of a slice
7
+func handleJSONArgs(args []string, attributes map[string]bool) []string {
8
+	if len(args) == 0 {
9
+		return []string{}
10
+	}
11
+
12
+	if attributes != nil && attributes["json"] {
13
+		return args
14
+	}
15
+
16
+	// literal string command, not an exec array
17
+	return []string{strings.Join(args, " ")}
18
+}
0 19
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package instructions
1
+
2
+import "testing"
3
+
4
+type testCase struct {
5
+	name       string
6
+	args       []string
7
+	attributes map[string]bool
8
+	expected   []string
9
+}
10
+
11
+func initTestCases() []testCase {
12
+	testCases := []testCase{}
13
+
14
+	testCases = append(testCases, testCase{
15
+		name:       "empty args",
16
+		args:       []string{},
17
+		attributes: make(map[string]bool),
18
+		expected:   []string{},
19
+	})
20
+
21
+	jsonAttributes := make(map[string]bool)
22
+	jsonAttributes["json"] = true
23
+
24
+	testCases = append(testCases, testCase{
25
+		name:       "json attribute with one element",
26
+		args:       []string{"foo"},
27
+		attributes: jsonAttributes,
28
+		expected:   []string{"foo"},
29
+	})
30
+
31
+	testCases = append(testCases, testCase{
32
+		name:       "json attribute with two elements",
33
+		args:       []string{"foo", "bar"},
34
+		attributes: jsonAttributes,
35
+		expected:   []string{"foo", "bar"},
36
+	})
37
+
38
+	testCases = append(testCases, testCase{
39
+		name:       "no attributes",
40
+		args:       []string{"foo", "bar"},
41
+		attributes: nil,
42
+		expected:   []string{"foo bar"},
43
+	})
44
+
45
+	return testCases
46
+}
47
+
48
+func TestHandleJSONArgs(t *testing.T) {
49
+	testCases := initTestCases()
50
+
51
+	for _, test := range testCases {
52
+		arguments := handleJSONArgs(test.args, test.attributes)
53
+
54
+		if len(arguments) != len(test.expected) {
55
+			t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments))
56
+		}
57
+
58
+		for i := range test.expected {
59
+			if arguments[i] != test.expected[i] {
60
+				t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i])
61
+			}
62
+		}
63
+	}
64
+}
... ...
@@ -124,7 +124,6 @@ func (b *Builder) commitContainer(dispatchState *dispatchState, id string, conta
124 124
 	}
125 125
 
126 126
 	dispatchState.imageID = imageID
127
-	b.buildStages.update(imageID)
128 127
 	return nil
129 128
 }
130 129
 
... ...
@@ -164,7 +163,6 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC
164 164
 
165 165
 	state.imageID = exportedImage.ImageID()
166 166
 	b.imageSources.Add(newImageMount(exportedImage, newLayer))
167
-	b.buildStages.update(state.imageID)
168 167
 	return nil
169 168
 }
170 169
 
... ...
@@ -460,7 +458,6 @@ func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.
460 460
 	fmt.Fprint(b.Stdout, " ---> Using cache\n")
461 461
 
462 462
 	dispatchState.imageID = cachedID
463
-	b.buildStages.update(dispatchState.imageID)
464 463
 	return true, nil
465 464
 }
466 465
 
467 466
deleted file mode 100644
... ...
@@ -1,19 +0,0 @@
1
-package dockerfile
2
-
3
-import "strings"
4
-
5
-// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile
6
-// for exec form it returns untouched args slice
7
-// for shell form it returns concatenated args as the first element of a slice
8
-func handleJSONArgs(args []string, attributes map[string]bool) []string {
9
-	if len(args) == 0 {
10
-		return []string{}
11
-	}
12
-
13
-	if attributes != nil && attributes["json"] {
14
-		return args
15
-	}
16
-
17
-	// literal string command, not an exec array
18
-	return []string{strings.Join(args, " ")}
19
-}
20 1
deleted file mode 100644
... ...
@@ -1,65 +0,0 @@
1
-package dockerfile
2
-
3
-import "testing"
4
-
5
-type testCase struct {
6
-	name       string
7
-	args       []string
8
-	attributes map[string]bool
9
-	expected   []string
10
-}
11
-
12
-func initTestCases() []testCase {
13
-	testCases := []testCase{}
14
-
15
-	testCases = append(testCases, testCase{
16
-		name:       "empty args",
17
-		args:       []string{},
18
-		attributes: make(map[string]bool),
19
-		expected:   []string{},
20
-	})
21
-
22
-	jsonAttributes := make(map[string]bool)
23
-	jsonAttributes["json"] = true
24
-
25
-	testCases = append(testCases, testCase{
26
-		name:       "json attribute with one element",
27
-		args:       []string{"foo"},
28
-		attributes: jsonAttributes,
29
-		expected:   []string{"foo"},
30
-	})
31
-
32
-	testCases = append(testCases, testCase{
33
-		name:       "json attribute with two elements",
34
-		args:       []string{"foo", "bar"},
35
-		attributes: jsonAttributes,
36
-		expected:   []string{"foo", "bar"},
37
-	})
38
-
39
-	testCases = append(testCases, testCase{
40
-		name:       "no attributes",
41
-		args:       []string{"foo", "bar"},
42
-		attributes: nil,
43
-		expected:   []string{"foo bar"},
44
-	})
45
-
46
-	return testCases
47
-}
48
-
49
-func TestHandleJSONArgs(t *testing.T) {
50
-	testCases := initTestCases()
51
-
52
-	for _, test := range testCases {
53
-		arguments := handleJSONArgs(test.args, test.attributes)
54
-
55
-		if len(arguments) != len(test.expected) {
56
-			t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments))
57
-		}
58
-
59
-		for i := range test.expected {
60
-			if arguments[i] != test.expected[i] {
61
-				t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i])
62
-			}
63
-		}
64
-	}
65
-}
... ...
@@ -438,6 +438,82 @@ func (s *DockerSuite) TestBuildChownOnCopy(c *check.C) {
438 438
 	assert.Contains(c, string(out), "Successfully built")
439 439
 }
440 440
 
441
+func (s *DockerSuite) TestBuildCopyCacheOnFileChange(c *check.C) {
442
+
443
+	dockerfile := `FROM busybox
444
+COPY file /file`
445
+
446
+	ctx1 := fakecontext.New(c, "",
447
+		fakecontext.WithDockerfile(dockerfile),
448
+		fakecontext.WithFile("file", "foo"))
449
+	ctx2 := fakecontext.New(c, "",
450
+		fakecontext.WithDockerfile(dockerfile),
451
+		fakecontext.WithFile("file", "bar"))
452
+
453
+	var build = func(ctx *fakecontext.Fake) string {
454
+		res, body, err := request.Post("/build",
455
+			request.RawContent(ctx.AsTarReader(c)),
456
+			request.ContentType("application/x-tar"))
457
+
458
+		require.NoError(c, err)
459
+		assert.Equal(c, http.StatusOK, res.StatusCode)
460
+
461
+		out, err := request.ReadBody(body)
462
+
463
+		ids := getImageIDsFromBuild(c, out)
464
+		return ids[len(ids)-1]
465
+	}
466
+
467
+	id1 := build(ctx1)
468
+	id2 := build(ctx1)
469
+	id3 := build(ctx2)
470
+
471
+	if id1 != id2 {
472
+		c.Fatal("didn't use the cache")
473
+	}
474
+	if id1 == id3 {
475
+		c.Fatal("COPY With different source file should not share same cache")
476
+	}
477
+}
478
+
479
+func (s *DockerSuite) TestBuildAddCacheOnFileChange(c *check.C) {
480
+
481
+	dockerfile := `FROM busybox
482
+ADD file /file`
483
+
484
+	ctx1 := fakecontext.New(c, "",
485
+		fakecontext.WithDockerfile(dockerfile),
486
+		fakecontext.WithFile("file", "foo"))
487
+	ctx2 := fakecontext.New(c, "",
488
+		fakecontext.WithDockerfile(dockerfile),
489
+		fakecontext.WithFile("file", "bar"))
490
+
491
+	var build = func(ctx *fakecontext.Fake) string {
492
+		res, body, err := request.Post("/build",
493
+			request.RawContent(ctx.AsTarReader(c)),
494
+			request.ContentType("application/x-tar"))
495
+
496
+		require.NoError(c, err)
497
+		assert.Equal(c, http.StatusOK, res.StatusCode)
498
+
499
+		out, err := request.ReadBody(body)
500
+
501
+		ids := getImageIDsFromBuild(c, out)
502
+		return ids[len(ids)-1]
503
+	}
504
+
505
+	id1 := build(ctx1)
506
+	id2 := build(ctx1)
507
+	id3 := build(ctx2)
508
+
509
+	if id1 != id2 {
510
+		c.Fatal("didn't use the cache")
511
+	}
512
+	if id1 == id3 {
513
+		c.Fatal("COPY With different source file should not share same cache")
514
+	}
515
+}
516
+
441 517
 func (s *DockerSuite) TestBuildWithSession(c *check.C) {
442 518
 	testRequires(c, ExperimentalDaemon)
443 519
 
... ...
@@ -1173,12 +1173,13 @@ func (s *DockerSuite) TestBuildForceRm(c *check.C) {
1173 1173
 	containerCountBefore := getContainerCount(c)
1174 1174
 	name := "testbuildforcerm"
1175 1175
 
1176
-	buildImage(name, cli.WithFlags("--force-rm"), build.WithBuildContext(c,
1177
-		build.WithFile("Dockerfile", `FROM `+minimalBaseImage()+`
1176
+	r := buildImage(name, cli.WithFlags("--force-rm"), build.WithBuildContext(c,
1177
+		build.WithFile("Dockerfile", `FROM busybox
1178 1178
 	RUN true
1179
-	RUN thiswillfail`))).Assert(c, icmd.Expected{
1180
-		ExitCode: 1,
1181
-	})
1179
+	RUN thiswillfail`)))
1180
+	if r.ExitCode != 1 && r.ExitCode != 127 { // different on Linux / Windows
1181
+		c.Fatalf("Wrong exit code")
1182
+	}
1182 1183
 
1183 1184
 	containerCountAfter := getContainerCount(c)
1184 1185
 	if containerCountBefore != containerCountAfter {
... ...
@@ -4542,7 +4543,6 @@ func (s *DockerSuite) TestBuildBuildTimeArgOverrideEnvDefinedBeforeArg(c *check.
4542 4542
 }
4543 4543
 
4544 4544
 func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
4545
-	testRequires(c, DaemonIsLinux) // Windows does not support ARG
4546 4545
 	imgName := "bldvarstest"
4547 4546
 
4548 4547
 	wdVar := "WDIR"
... ...
@@ -4559,6 +4559,10 @@ func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
4559 4559
 	userVal := "testUser"
4560 4560
 	volVar := "VOL"
4561 4561
 	volVal := "/testVol/"
4562
+	if DaemonIsWindows() {
4563
+		volVal = "C:\\testVol"
4564
+		wdVal = "C:\\tmp"
4565
+	}
4562 4566
 
4563 4567
 	buildImageSuccessfully(c, imgName,
4564 4568
 		cli.WithFlags(
... ...
@@ -4594,7 +4598,7 @@ func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
4594 4594
 	)
4595 4595
 
4596 4596
 	res := inspectField(c, imgName, "Config.WorkingDir")
4597
-	c.Check(res, check.Equals, filepath.ToSlash(wdVal))
4597
+	c.Check(filepath.ToSlash(res), check.Equals, filepath.ToSlash(wdVal))
4598 4598
 
4599 4599
 	var resArr []string
4600 4600
 	inspectFieldAndUnmarshall(c, imgName, "Config.Env", &resArr)