Browse code

Refactor pkg/build/builder

Rodolfo Carvalho authored on 2015/09/19 00:05:30
Showing 9 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,32 @@
0
+package builder
1
+
2
+import "github.com/openshift/origin/pkg/build/api"
3
+
4
+// A KeyValue can be used to build ordered lists of key-value pairs.
5
+type KeyValue struct {
6
+	Key   string
7
+	Value string
8
+}
9
+
10
+// buildInfo returns a slice of KeyValue pairs with build metadata to be
11
+// inserted into Docker images produced by build.
12
+func buildInfo(build *api.Build) []KeyValue {
13
+	kv := []KeyValue{
14
+		{"OPENSHIFT_BUILD_NAME", build.Name},
15
+		{"OPENSHIFT_BUILD_NAMESPACE", build.Namespace},
16
+		{"OPENSHIFT_BUILD_SOURCE", build.Spec.Source.Git.URI},
17
+	}
18
+	if build.Spec.Source.Git.Ref != "" {
19
+		kv = append(kv, KeyValue{"OPENSHIFT_BUILD_REFERENCE", build.Spec.Source.Git.Ref})
20
+	}
21
+	if build.Spec.Revision != nil && build.Spec.Revision.Git != nil && build.Spec.Revision.Git.Commit != "" {
22
+		kv = append(kv, KeyValue{"OPENSHIFT_BUILD_COMMIT", build.Spec.Revision.Git.Commit})
23
+	}
24
+	if build.Spec.Strategy.Type == api.SourceBuildStrategyType {
25
+		env := build.Spec.Strategy.SourceStrategy.Env
26
+		for _, e := range env {
27
+			kv = append(kv, KeyValue{e.Name, e.Value})
28
+		}
29
+	}
30
+	return kv
31
+}
0 32
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package builder
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	kapi "k8s.io/kubernetes/pkg/api"
7
+
8
+	"github.com/openshift/origin/pkg/build/api"
9
+)
10
+
11
+func TestBuildInfo(t *testing.T) {
12
+	b := &api.Build{
13
+		ObjectMeta: kapi.ObjectMeta{
14
+			Name:      "sample-app",
15
+			Namespace: "default",
16
+		},
17
+		Spec: api.BuildSpec{
18
+			Source: api.BuildSource{
19
+				Git: &api.GitBuildSource{
20
+					URI: "github.com/openshift/sample-app",
21
+					Ref: "master",
22
+				},
23
+			},
24
+			Strategy: api.BuildStrategy{
25
+				Type: api.SourceBuildStrategyType,
26
+				SourceStrategy: &api.SourceBuildStrategy{
27
+					Env: []kapi.EnvVar{
28
+						{Name: "RAILS_ENV", Value: "production"},
29
+					},
30
+				},
31
+			},
32
+			Revision: &api.SourceRevision{
33
+				Git: &api.GitSourceRevision{
34
+					Commit: "1575a90c569a7cc0eea84fbd3304d9df37c9f5ee",
35
+				},
36
+			},
37
+		},
38
+	}
39
+	got := buildInfo(b)
40
+	want := []KeyValue{
41
+		{"OPENSHIFT_BUILD_NAME", "sample-app"},
42
+		{"OPENSHIFT_BUILD_NAMESPACE", "default"},
43
+		{"OPENSHIFT_BUILD_SOURCE", "github.com/openshift/sample-app"},
44
+		{"OPENSHIFT_BUILD_REFERENCE", "master"},
45
+		{"OPENSHIFT_BUILD_COMMIT", "1575a90c569a7cc0eea84fbd3304d9df37c9f5ee"},
46
+		{"RAILS_ENV", "production"},
47
+	}
48
+	if !reflect.DeepEqual(got, want) {
49
+		t.Errorf("buildInfo(%+v) = %+v; want %+v", b, got, want)
50
+	}
51
+}
... ...
@@ -1,10 +1,7 @@
1 1
 package builder
2 2
 
3 3
 import (
4
-	"bytes"
5
-	"errors"
6 4
 	"fmt"
7
-	"io"
8 5
 	"io/ioutil"
9 6
 	"net"
10 7
 	"net/url"
... ...
@@ -19,13 +16,14 @@ import (
19 19
 	"github.com/golang/glog"
20 20
 	kapi "k8s.io/kubernetes/pkg/api"
21 21
 
22
-	"github.com/openshift/origin/pkg/build/api"
23
-	"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg"
24
-	"github.com/openshift/origin/pkg/util/docker/dockerfile"
25 22
 	s2iapi "github.com/openshift/source-to-image/pkg/api"
26 23
 	"github.com/openshift/source-to-image/pkg/scm/git"
27 24
 	"github.com/openshift/source-to-image/pkg/tar"
28 25
 	"github.com/openshift/source-to-image/pkg/util"
26
+
27
+	"github.com/openshift/origin/pkg/build/api"
28
+	"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg"
29
+	"github.com/openshift/origin/pkg/util/docker/dockerfile"
29 30
 )
30 31
 
31 32
 const (
... ...
@@ -48,17 +46,6 @@ type DockerBuilder struct {
48 48
 	urlTimeout   time.Duration
49 49
 }
50 50
 
51
-// MetaInstuction represent an Docker instruction used for adding metadata
52
-// to Dockerfile
53
-type MetaInstruction string
54
-
55
-const (
56
-	// Label represents the LABEL Docker instruction
57
-	Label MetaInstruction = "LABEL"
58
-	// Env represents the ENV Docker instruction
59
-	Env MetaInstruction = "ENV"
60
-)
61
-
62 51
 // NewDockerBuilder creates a new instance of DockerBuilder
63 52
 func NewDockerBuilder(dockerClient DockerClient, build *api.Build) *DockerBuilder {
64 53
 	return &DockerBuilder{
... ...
@@ -229,44 +216,38 @@ func (d *DockerBuilder) addBuildParameters(dir string) error {
229 229
 		dockerfilePath = filepath.Join(dir, d.build.Spec.Source.ContextDir, "Dockerfile")
230 230
 	}
231 231
 
232
-	fileStat, err := os.Lstat(dockerfilePath)
232
+	f, err := os.Open(dockerfilePath)
233 233
 	if err != nil {
234 234
 		return err
235 235
 	}
236 236
 
237
-	filePerm := fileStat.Mode()
238
-
239
-	fileData, err := ioutil.ReadFile(dockerfilePath)
237
+	// Parse the Dockerfile.
238
+	node, err := parser.Parse(f)
240 239
 	if err != nil {
241 240
 		return err
242 241
 	}
243 242
 
244
-	var newFileData string
243
+	// Update base image if build strategy specifies the From field.
245 244
 	if d.build.Spec.Strategy.DockerStrategy.From != nil && d.build.Spec.Strategy.DockerStrategy.From.Kind == "DockerImage" {
246
-		newFileData, err = replaceValidCmd(dockercmd.From, d.build.Spec.Strategy.DockerStrategy.From.Name, fileData)
245
+		err := replaceLastFrom(node, d.build.Spec.Strategy.DockerStrategy.From.Name)
247 246
 		if err != nil {
248 247
 			return err
249 248
 		}
250
-	} else {
251
-		newFileData = newFileData + string(fileData)
252 249
 	}
253 250
 
254
-	envVars := getBuildEnvVars(d.build)
255
-	newFileData = appendMetadata(Env, newFileData, envVars)
256
-
257
-	labels := map[string]string{}
258
-	sourceInfo := d.git.GetInfo(dir)
259
-	if len(d.build.Spec.Source.ContextDir) > 0 {
260
-		sourceInfo.ContextDir = d.build.Spec.Source.ContextDir
251
+	// Append build info as environment variables.
252
+	err = appendEnv(node, d.buildInfo())
253
+	if err != nil {
254
+		return err
261 255
 	}
262
-	labels = util.GenerateLabelsFromSourceInfo(labels, sourceInfo, api.DefaultDockerLabelNamespace)
263
-	newFileData = appendMetadata(Label, newFileData, labels)
264 256
 
265
-	node, err := parser.Parse(strings.NewReader(newFileData))
257
+	// Append build labels.
258
+	err = appendLabel(node, d.buildLabels(dir))
266 259
 	if err != nil {
267 260
 		return err
268 261
 	}
269 262
 
263
+	// Insert environment variables defined in the build strategy.
270 264
 	err = insertEnvAfterFrom(node, d.build.Spec.Strategy.DockerStrategy.Env)
271 265
 	if err != nil {
272 266
 		return err
... ...
@@ -274,156 +255,123 @@ func (d *DockerBuilder) addBuildParameters(dir string) error {
274 274
 
275 275
 	instructions := dockerfile.ParseTreeToDockerfile(node)
276 276
 
277
-	return ioutil.WriteFile(dockerfilePath, instructions, filePerm)
277
+	// Overwrite the Dockerfile.
278
+	fi, err := f.Stat()
279
+	if err != nil {
280
+		return err
281
+	}
282
+	return ioutil.WriteFile(dockerfilePath, instructions, fi.Mode())
278 283
 }
279 284
 
280
-// appendMetadata appends a Docker instruction that adds metadata values
281
-// to a string containing a valid Dockerfile.
282
-func appendMetadata(inst MetaInstruction, fileData string, envVars map[string]string) string {
283
-	if !strings.HasSuffix(fileData, "\n") {
284
-		fileData += "\n"
285
-	}
286
-	first := true
287
-	for k, v := range envVars {
288
-		if first {
289
-			fileData += fmt.Sprintf("%s %s=%+q", inst, k, v)
290
-			first = false
291
-		} else {
292
-			fileData += fmt.Sprintf(" \\\n\t%s=%+q", k, v)
293
-		}
285
+// buildInfo converts the buildInfo output to a format that appendEnv can
286
+// consume.
287
+func (d *DockerBuilder) buildInfo() []dockerfile.KeyValue {
288
+	bi := buildInfo(d.build)
289
+	kv := make([]dockerfile.KeyValue, len(bi))
290
+	for i, item := range bi {
291
+		kv[i] = dockerfile.KeyValue{Key: item.Key, Value: item.Value}
294 292
 	}
295
-	fileData += "\n"
296
-	return fileData
293
+	return kv
297 294
 }
298 295
 
299
-// invalidCmdErr represents an error returned from replaceValidCmd
300
-// when an invalid Dockerfile command has been passed to
301
-// replaceValidCmd
302
-var invalidCmdErr = errors.New("invalid Dockerfile command")
303
-
304
-// replaceCmdErr represents an error returned from replaceValidCmd
305
-// when a command which has more than one valid occurrences inside
306
-// a Dockerfile has been passed or the specified command cannot
307
-// be found
308
-var replaceCmdErr = errors.New("cannot replace given Dockerfile command")
309
-
310
-// replaceValidCmd replaces the valid occurrence of a command
311
-// in a Dockerfile with the given replaceArgs
312
-func replaceValidCmd(cmd, replaceArgs string, fileData []byte) (string, error) {
313
-	if _, ok := dockercmd.Commands[cmd]; !ok {
314
-		return "", invalidCmdErr
315
-	}
316
-	buf := bytes.NewBuffer(fileData)
317
-	// Parse with Docker parser
318
-	node, err := parser.Parse(buf)
319
-	if err != nil {
320
-		return "", errors.New("cannot parse Dockerfile: " + err.Error())
296
+// buildLabels returns a slice of KeyValue pairs in a format that appendEnv can
297
+// consume.
298
+func (d *DockerBuilder) buildLabels(dir string) []dockerfile.KeyValue {
299
+	labels := map[string]string{}
300
+	sourceInfo := d.git.GetInfo(dir)
301
+	if len(d.build.Spec.Source.ContextDir) > 0 {
302
+		sourceInfo.ContextDir = d.build.Spec.Source.ContextDir
321 303
 	}
304
+	labels = util.GenerateLabelsFromSourceInfo(labels, sourceInfo, api.DefaultDockerLabelNamespace)
305
+	kv := make([]dockerfile.KeyValue, len(labels))
306
+	i := 0
307
+	for k, v := range labels {
308
+		kv[i] = dockerfile.KeyValue{Key: k, Value: v}
309
+		i++
310
+	}
311
+	return kv
312
+}
322 313
 
323
-	pos := traverseAST(cmd, node)
324
-	if pos == 0 {
325
-		return "", replaceCmdErr
314
+// setupPullSecret provides a Docker authentication configuration when the
315
+// PullSecret is specified.
316
+func (d *DockerBuilder) setupPullSecret() (*docker.AuthConfigurations, error) {
317
+	if len(os.Getenv(dockercfg.PullAuthType)) == 0 {
318
+		return nil, nil
326 319
 	}
320
+	r, err := os.Open(os.Getenv(dockercfg.PullAuthType))
321
+	if err != nil {
322
+		return nil, fmt.Errorf("'%s': %s", os.Getenv(dockercfg.PullAuthType), err)
323
+	}
324
+	return docker.NewAuthConfigurations(r)
325
+}
327 326
 
328
-	// Re-initialize the buffer
329
-	buf = bytes.NewBuffer(fileData)
330
-	var newFileData string
331
-	var index int
332
-	var replaceNextLn bool
333
-	for {
334
-		line, err := buf.ReadString('\n')
335
-		if err != nil && err != io.EOF {
336
-			return "", err
327
+// dockerBuild performs a docker build on the source that has been retrieved
328
+func (d *DockerBuilder) dockerBuild(dir string) error {
329
+	var noCache bool
330
+	var forcePull bool
331
+	if d.build.Spec.Strategy.DockerStrategy != nil {
332
+		if d.build.Spec.Source.ContextDir != "" {
333
+			dir = filepath.Join(dir, d.build.Spec.Source.ContextDir)
337 334
 		}
338
-		line = strings.TrimSpace(line)
339
-
340
-		// The current line starts with the specified command (cmd)
341
-		if strings.HasPrefix(strings.ToUpper(line), strings.ToUpper(cmd)) {
342
-			index++
343
-
344
-			// The current line finishes on a backslash.
345
-			// All we need to do is replace the next line
346
-			// with our specified replaceArgs
347
-			if line[len(line)-1:] == "\\" && index == pos {
348
-				replaceNextLn = true
349
-
350
-				args := strings.Split(line, " ")
351
-				if len(args) > 2 {
352
-					// Keep just our Dockerfile command and the backslash
353
-					newFileData += args[0] + " \\" + "\n"
354
-				} else {
355
-					newFileData += line + "\n"
356
-				}
357
-				continue
358
-			}
335
+		noCache = d.build.Spec.Strategy.DockerStrategy.NoCache
336
+		forcePull = d.build.Spec.Strategy.DockerStrategy.ForcePull
337
+	}
338
+	auth, err := d.setupPullSecret()
339
+	if err != nil {
340
+		return err
341
+	}
342
+	return buildImage(d.dockerClient, dir, noCache, d.build.Spec.Output.To.Name, d.tar, auth, forcePull)
343
+}
359 344
 
360
-			// Normal ending line
361
-			if index == pos {
362
-				line = fmt.Sprintf("%s %s", strings.ToUpper(cmd), replaceArgs)
345
+// replaceLastFrom changes the last FROM instruction of node to point to the
346
+// base image image.
347
+func replaceLastFrom(node *parser.Node, image string) error {
348
+	if node == nil {
349
+		return nil
350
+	}
351
+	for i := len(node.Children) - 1; i >= 0; i-- {
352
+		child := node.Children[i]
353
+		if child != nil && child.Value == dockercmd.From {
354
+			from, err := dockerfile.From(image)
355
+			if err != nil {
356
+				return err
363 357
 			}
364
-		}
365
-
366
-		// Previous line ended on a backslash
367
-		// This line contains command arguments
368
-		if replaceNextLn {
369
-			if line[len(line)-1:] == "\\" {
370
-				// Ignore all successive lines terminating on a backslash
371
-				// since they all are going to be replaced by replaceArgs
372
-				continue
358
+			fromTree, err := parser.Parse(strings.NewReader(from))
359
+			if err != nil {
360
+				return err
373 361
 			}
374
-			replaceNextLn = false
375
-			line = replaceArgs
362
+			node.Children[i] = fromTree.Children[0]
363
+			return nil
376 364
 		}
377
-
378
-		if err == io.EOF {
379
-			// Otherwise, the new Dockerfile will have one newline
380
-			// more in the end
381
-			newFileData += line
382
-			break
383
-		}
384
-		newFileData += line + "\n"
385 365
 	}
366
+	return nil
367
+}
386 368
 
387
-	// Parse output for validation
388
-	buf = bytes.NewBuffer([]byte(newFileData))
389
-	if _, err := parser.Parse(buf); err != nil {
390
-		return "", errors.New("cannot parse new Dockerfile: " + err.Error())
391
-	}
369
+// appendEnv appends an ENV Dockerfile instruction as the last child of node
370
+// with keys and values from m.
371
+func appendEnv(node *parser.Node, m []dockerfile.KeyValue) error {
372
+	return appendKeyValueInstruction(dockerfile.Env, node, m)
373
+}
392 374
 
393
-	return newFileData, nil
375
+// appendLabel appends a LABEL Dockerfile instruction as the last child of node
376
+// with keys and values from m.
377
+func appendLabel(node *parser.Node, m []dockerfile.KeyValue) error {
378
+	return appendKeyValueInstruction(dockerfile.Label, node, m)
394 379
 }
395 380
 
396
-// traverseAST traverses the Abstract Syntax Tree output
397
-// from the Docker parser and returns the valid position
398
-// of the command it was requested to look for.
399
-//
400
-// Note that this function is intended to be used with
401
-// Dockerfile commands that should be specified only once
402
-// in a Dockerfile (FROM, CMD, ENTRYPOINT)
403
-func traverseAST(cmd string, node *parser.Node) int {
404
-	switch cmd {
405
-	case dockercmd.From, dockercmd.Entrypoint, dockercmd.Cmd:
406
-	default:
407
-		return 0
408
-	}
409
-
410
-	index := 0
411
-	if node.Value == cmd {
412
-		index++
413
-	}
414
-	for _, n := range node.Children {
415
-		index += traverseAST(cmd, n)
416
-	}
417
-	if node.Next != nil {
418
-		for n := node.Next; n != nil; n = n.Next {
419
-			if len(n.Children) > 0 {
420
-				index += traverseAST(cmd, n)
421
-			} else if n.Value == cmd {
422
-				index++
423
-			}
424
-		}
381
+// appendKeyValueInstruction is a primitive used to avoid code duplication.
382
+// Callers should use a derivative of this such as appendEnv or appendLabel.
383
+// appendKeyValueInstruction appends a Dockerfile instruction with key-value
384
+// syntax created by f as the last child of node with keys and values from m.
385
+func appendKeyValueInstruction(f func([]dockerfile.KeyValue) (string, error), node *parser.Node, m []dockerfile.KeyValue) error {
386
+	if node == nil {
387
+		return nil
425 388
 	}
426
-	return index
389
+	instruction, err := f(m)
390
+	if err != nil {
391
+		return err
392
+	}
393
+	return dockerfile.InsertInstructions(node, len(node.Children), instruction)
427 394
 }
428 395
 
429 396
 // insertEnvAfterFrom inserts an ENV instruction with the environment variables
... ...
@@ -456,34 +404,3 @@ func insertEnvAfterFrom(node *parser.Node, env []kapi.EnvVar) error {
456 456
 
457 457
 	return nil
458 458
 }
459
-
460
-// setupPullSecret provides a Docker authentication configuration when the
461
-// PullSecret is specified.
462
-func (d *DockerBuilder) setupPullSecret() (*docker.AuthConfigurations, error) {
463
-	if len(os.Getenv(dockercfg.PullAuthType)) == 0 {
464
-		return nil, nil
465
-	}
466
-	r, err := os.Open(os.Getenv(dockercfg.PullAuthType))
467
-	if err != nil {
468
-		return nil, fmt.Errorf("'%s': %s", os.Getenv(dockercfg.PullAuthType), err)
469
-	}
470
-	return docker.NewAuthConfigurations(r)
471
-}
472
-
473
-// dockerBuild performs a docker build on the source that has been retrieved
474
-func (d *DockerBuilder) dockerBuild(dir string) error {
475
-	var noCache bool
476
-	var forcePull bool
477
-	if d.build.Spec.Strategy.DockerStrategy != nil {
478
-		if d.build.Spec.Source.ContextDir != "" {
479
-			dir = filepath.Join(dir, d.build.Spec.Source.ContextDir)
480
-		}
481
-		noCache = d.build.Spec.Strategy.DockerStrategy.NoCache
482
-		forcePull = d.build.Spec.Strategy.DockerStrategy.ForcePull
483
-	}
484
-	auth, err := d.setupPullSecret()
485
-	if err != nil {
486
-		return err
487
-	}
488
-	return buildImage(d.dockerClient, dir, noCache, d.build.Spec.Output.To.Name, d.tar, auth, forcePull)
489
-}
... ...
@@ -1,259 +1,16 @@
1 1
 package builder
2 2
 
3 3
 import (
4
-	"bytes"
5
-	"log"
6 4
 	"reflect"
7 5
 	"strings"
8 6
 	"testing"
9 7
 
10
-	dockercmd "github.com/docker/docker/builder/command"
11 8
 	"github.com/docker/docker/builder/parser"
12 9
 	kapi "k8s.io/kubernetes/pkg/api"
13 10
 
14 11
 	"github.com/openshift/origin/pkg/util/docker/dockerfile"
15 12
 )
16 13
 
17
-func TestReplaceValidCmd(t *testing.T) {
18
-	tests := []struct {
19
-		name           string
20
-		cmd            string
21
-		replaceArgs    string
22
-		fileData       []byte
23
-		expectedOutput string
24
-		expectedDiffs  int
25
-		expectedErr    error
26
-	}{
27
-		{
28
-			name:           "from-replacement",
29
-			cmd:            dockercmd.From,
30
-			replaceArgs:    "other/image",
31
-			fileData:       []byte(dockerFile),
32
-			expectedOutput: expectedFROM,
33
-			expectedDiffs:  1,
34
-			expectedErr:    nil,
35
-		},
36
-		{
37
-			name:           "run-replacement",
38
-			cmd:            dockercmd.Run,
39
-			replaceArgs:    "This test kind-of-fails before string replacement so this string won't be used",
40
-			fileData:       []byte(dockerFile),
41
-			expectedOutput: "",
42
-			expectedErr:    replaceCmdErr,
43
-		},
44
-		{
45
-			name:           "invalid-dockerfile-cmd",
46
-			cmd:            "blabla",
47
-			replaceArgs:    "This test fails at start so this string won't be used",
48
-			fileData:       []byte(dockerFile),
49
-			expectedOutput: "",
50
-			expectedErr:    invalidCmdErr,
51
-		},
52
-		{
53
-			name:           "no-cmd-in-dockerfile",
54
-			cmd:            dockercmd.Cmd,
55
-			replaceArgs:    "runme.sh",
56
-			fileData:       []byte(dockerFile),
57
-			expectedOutput: "",
58
-			expectedErr:    replaceCmdErr,
59
-		},
60
-		{
61
-			name:           "trailing-slash",
62
-			cmd:            dockercmd.From,
63
-			replaceArgs:    "rhel",
64
-			fileData:       []byte(trSlashFile),
65
-			expectedOutput: expectedtrSlashFile,
66
-			expectedDiffs:  1,
67
-			expectedErr:    nil,
68
-		},
69
-		{
70
-			name:           "multiple trailing slashes plus plus",
71
-			cmd:            dockercmd.From,
72
-			replaceArgs:    "scratch",
73
-			fileData:       []byte(trickierFile),
74
-			expectedOutput: expectedTrickierFile,
75
-			expectedDiffs:  1,
76
-			expectedErr:    nil,
77
-		},
78
-	}
79
-
80
-	for _, test := range tests {
81
-		out, err := replaceValidCmd(test.cmd, test.replaceArgs, test.fileData)
82
-		if err != test.expectedErr {
83
-			t.Errorf("%s: Unexpected error: Expected %v, got %v", test.name, test.expectedErr, err)
84
-		}
85
-		if out != test.expectedOutput {
86
-			t.Errorf("%s: Unexpected output:\n\nExpected:\n%s\n(length: %d)\n\ngot:\n%s\n(length: %d)",
87
-				test.name, test.expectedOutput, len(test.expectedOutput), out, len(out))
88
-		}
89
-	}
90
-
91
-	// Re-use the tests above
92
-	var buf *bytes.Buffer
93
-	for _, test := range tests {
94
-		buf = bytes.NewBuffer([]byte(test.fileData))
95
-		original, err := parser.Parse(buf)
96
-		if err != nil {
97
-			log.Println(err)
98
-		}
99
-		repl, err := replaceValidCmd(test.cmd, test.replaceArgs, test.fileData)
100
-		if err != nil {
101
-			log.Println(err)
102
-		}
103
-		buf = bytes.NewBuffer([]byte(repl))
104
-		edited, err := parser.Parse(buf)
105
-		if err != nil {
106
-			log.Println(err)
107
-		}
108
-
109
-		diff := cmpASTs(original, edited)
110
-
111
-		if diff != test.expectedDiffs {
112
-			t.Errorf("%s: Edit mismatch, expected %d edit(s), got %d", test.name, test.expectedDiffs, diff)
113
-		}
114
-	}
115
-}
116
-
117
-// cmpASTs compares two Abstract Syntax Trees and returns the
118
-// amount of differences between them
119
-func cmpASTs(original *parser.Node, edited *parser.Node) int {
120
-	index := 0
121
-	if original.Value != edited.Value {
122
-		index++
123
-	}
124
-
125
-	originalChildren := make([]*parser.Node, 0)
126
-	for _, n := range original.Children {
127
-		originalChildren = append(originalChildren, n)
128
-	}
129
-	editedChildren := make([]*parser.Node, 0)
130
-	for _, n := range edited.Children {
131
-		editedChildren = append(editedChildren, n)
132
-	}
133
-	for i := 0; i < len(editedChildren); i++ {
134
-		index += cmpASTs(originalChildren[i], editedChildren[i])
135
-	}
136
-
137
-	if original.Next != nil && edited.Next != nil {
138
-		index += cmpASTs(original.Next, edited.Next)
139
-	} else if original.Next != edited.Next {
140
-		index++
141
-	}
142
-	return index
143
-}
144
-
145
-func TestTraverseAST(t *testing.T) {
146
-	tests := []struct {
147
-		name     string
148
-		cmd      string
149
-		fileData []byte
150
-		expected int
151
-	}{
152
-		{
153
-			name:     "dockerFile",
154
-			cmd:      dockercmd.Entrypoint,
155
-			fileData: []byte(dockerFile),
156
-			expected: 1,
157
-		},
158
-		{
159
-			name:     "dockerFile no newline",
160
-			cmd:      dockercmd.Entrypoint,
161
-			fileData: []byte(dockerFileNoNewline),
162
-			expected: 1,
163
-		},
164
-		{
165
-			name:     "expectedFROM",
166
-			cmd:      dockercmd.From,
167
-			fileData: []byte(expectedFROM),
168
-			expected: 2,
169
-		},
170
-		{
171
-			name:     "trSlashFile",
172
-			cmd:      dockercmd.Entrypoint,
173
-			fileData: []byte(trSlashFile),
174
-			expected: 0,
175
-		},
176
-		{
177
-			name:     "expectedtrSlashFile",
178
-			cmd:      dockercmd.Cmd,
179
-			fileData: []byte(expectedtrSlashFile),
180
-			expected: 1,
181
-		},
182
-	}
183
-
184
-	var buf *bytes.Buffer
185
-	for _, test := range tests {
186
-		buf = bytes.NewBuffer([]byte(test.fileData))
187
-		node, err := parser.Parse(buf)
188
-		if err != nil {
189
-			log.Println(err)
190
-		}
191
-
192
-		howMany := traverseAST(test.cmd, node)
193
-		if howMany != test.expected {
194
-			t.Errorf("Wrong result, expected %d, got %d", test.expected, howMany)
195
-		}
196
-	}
197
-}
198
-
199
-func TestAppendEnvVars(t *testing.T) {
200
-	tests := []struct {
201
-		name       string
202
-		dockerFile string
203
-		envVars    map[string]string
204
-		expected   string
205
-	}{
206
-		{
207
-			name:       "regular dockerFile",
208
-			dockerFile: dockerFile,
209
-			envVars:    map[string]string{"VAR1": "value1"},
210
-			expected:   dockerFile + "ENV VAR1=\"value1\"\n",
211
-		},
212
-		{
213
-			name:       "dockerFile with no newline",
214
-			dockerFile: dockerFileNoNewline,
215
-			envVars:    map[string]string{"VAR1": "value1"},
216
-			expected:   dockerFileNoNewline + "\nENV VAR1=\"value1\"\n",
217
-		},
218
-	}
219
-
220
-	for _, test := range tests {
221
-		result := appendMetadata(Env, test.dockerFile, test.envVars)
222
-		if result != test.expected {
223
-			t.Errorf("%s: unexpected result.\n\tExpected: %s\n\tGot: %s\n", test.name, test.expected, result)
224
-		}
225
-	}
226
-}
227
-
228
-func TestAppendLabels(t *testing.T) {
229
-	tests := []struct {
230
-		name       string
231
-		dockerFile string
232
-		labels     map[string]string
233
-		expected   string
234
-	}{
235
-		{
236
-			name:       "regular dockerFile",
237
-			dockerFile: dockerFile,
238
-			labels:     map[string]string{"LABEL1": "value1"},
239
-			expected:   dockerFile + "LABEL LABEL1=\"value1\"\n",
240
-		},
241
-		{
242
-			name:       "dockerFile with no newline",
243
-			dockerFile: dockerFileNoNewline,
244
-			labels:     map[string]string{"LABEL1": "value1"},
245
-			expected:   dockerFileNoNewline + "\nLABEL LABEL1=\"value1\"\n",
246
-		},
247
-	}
248
-
249
-	for _, test := range tests {
250
-		result := appendMetadata(Label, test.dockerFile, test.labels)
251
-		if result != test.expected {
252
-			t.Errorf("%s: unexpected result.\n\tExpected: %s\n\tGot: %s\n", test.name, test.expected, result)
253
-		}
254
-	}
255
-}
256
-
257 14
 func TestInsertEnvAfterFrom(t *testing.T) {
258 15
 	tests := map[string]struct {
259 16
 		original string
... ...
@@ -321,69 +78,54 @@ RUN echo "hello world"
321 321
 	}
322 322
 }
323 323
 
324
-const (
325
-	dockerFile = `
326
-FROM openshift/origin-base
327
-FROM candidate
328
-
329
-RUN mkdir -p /var/lib/openshift
330
-
331
-ADD bin/openshift        /usr/bin/openshift
332
-RUN ln -s /usr/bin/openshift /usr/bin/oc && \
333
-
334
-ENV HOME /root
335
-WORKDIR /var/lib/openshift
336
-ENTRYPOINT ["/usr/bin/openshift"]
337
-`
338
-	dockerFileNoNewline = `
339
-FROM openshift/origin-base
340
-FROM candidate
341
-
342
-RUN mkdir -p /var/lib/openshift
343
-
344
-ADD bin/openshift        /usr/bin/openshift
345
-RUN ln -s /usr/bin/openshift /usr/bin/oc && \
346
-
347
-ENV HOME /root
348
-WORKDIR /var/lib/openshift
349
-ENTRYPOINT ["/usr/bin/openshift"]`
350
-
351
-	expectedFROM = `
352
-FROM openshift/origin-base
353
-FROM other/image
354
-
355
-RUN mkdir -p /var/lib/openshift
356
-
357
-ADD bin/openshift        /usr/bin/openshift
358
-RUN ln -s /usr/bin/openshift /usr/bin/oc && \
359
-
360
-ENV HOME /root
361
-WORKDIR /var/lib/openshift
362
-ENTRYPOINT ["/usr/bin/openshift"]
363
-`
364
-
365
-	trSlashFile = `
366
-from \
367
-centos
368
-CMD "cat /etc/passwd"`
369
-
370
-	expectedtrSlashFile = `
371
-from \
372
-rhel
373
-CMD "cat /etc/passwd"`
374
-
375
-	trickierFile = `
376
-from centos \
377
-rhel \
378
-ubuntu
379
-
380
-CMD ["executable","param1","param2"]
381
-`
382
-
383
-	expectedTrickierFile = `
384
-from \
385
-scratch
386
-
387
-CMD ["executable","param1","param2"]
388
-`
389
-)
324
+func TestReplaceLastFrom(t *testing.T) {
325
+	tests := []struct {
326
+		original string
327
+		image    string
328
+		want     string
329
+	}{
330
+		{
331
+			original: `# no FROM instruction`,
332
+			image:    "centos",
333
+			want:     ``,
334
+		},
335
+		{
336
+			original: `FROM scratch
337
+# FROM busybox
338
+RUN echo "hello world"
339
+`,
340
+			image: "centos",
341
+			want: `FROM centos
342
+RUN echo "hello world"
343
+`,
344
+		},
345
+		{
346
+			original: `FROM scratch
347
+FROM busybox
348
+RUN echo "hello world"
349
+`,
350
+			image: "centos",
351
+			want: `FROM scratch
352
+FROM centos
353
+RUN echo "hello world"
354
+`,
355
+		},
356
+	}
357
+	for i, test := range tests {
358
+		got, err := parser.Parse(strings.NewReader(test.original))
359
+		if err != nil {
360
+			t.Errorf("test[%d]: %v", i, err)
361
+			continue
362
+		}
363
+		want, err := parser.Parse(strings.NewReader(test.want))
364
+		if err != nil {
365
+			t.Errorf("test[%d]: %v", i, err)
366
+			continue
367
+		}
368
+		replaceLastFrom(got, test.image)
369
+		if !reflect.DeepEqual(got, want) {
370
+			t.Errorf("test[%d]: replaceLastFrom(node, %+v) = %+v; want %+v", i, test.image, got, want)
371
+			t.Logf("resulting Dockerfile:\n%s", dockerfile.ParseTreeToDockerfile(got))
372
+		}
373
+	}
374
+}
... ...
@@ -58,7 +58,7 @@ func (s *STIBuilder) Build() error {
58 58
 		DockerCfgPath:  os.Getenv(dockercfg.PullAuthType),
59 59
 		Tag:            tag,
60 60
 		ScriptsURL:     s.build.Spec.Strategy.SourceStrategy.Scripts,
61
-		Environment:    getBuildEnvVars(s.build),
61
+		Environment:    buildEnvVars(s.build),
62 62
 		LabelNamespace: api.DefaultDockerLabelNamespace,
63 63
 		Incremental:    s.build.Spec.Strategy.SourceStrategy.Incremental,
64 64
 		ForcePull:      s.build.Spec.Strategy.SourceStrategy.ForcePull,
... ...
@@ -157,3 +157,20 @@ func (s *STIBuilder) Build() error {
157 157
 	}
158 158
 	return nil
159 159
 }
160
+
161
+// buildEnvVars returns a map with build metadata to be inserted into Docker
162
+// images produced by build. It transforms the output from buildInfo into the
163
+// input format expected by stiapi.Config.Environment.
164
+// Note that using a map has at least two downsides:
165
+// 1. The order of metadata KeyValue pairs is lost;
166
+// 2. In case of repeated Keys, the last Value takes precedence right here,
167
+//    instead of deferring what to do with repeated environment variables to the
168
+//    Docker runtime.
169
+func buildEnvVars(build *api.Build) map[string]string {
170
+	bi := buildInfo(build)
171
+	envVars := make(map[string]string, len(bi))
172
+	for _, item := range bi {
173
+		envVars[item.Key] = item.Value
174
+	}
175
+	return envVars
176
+}
160 177
deleted file mode 100644
... ...
@@ -1,30 +0,0 @@
1
-package builder
2
-
3
-import (
4
-	buildapi "github.com/openshift/origin/pkg/build/api"
5
-)
6
-
7
-// getBuildEnvVars returns a map with the environment variables that should be added
8
-// to the built image
9
-func getBuildEnvVars(build *buildapi.Build) map[string]string {
10
-	envVars := map[string]string{
11
-		"OPENSHIFT_BUILD_NAME":      build.Name,
12
-		"OPENSHIFT_BUILD_NAMESPACE": build.Namespace,
13
-		"OPENSHIFT_BUILD_SOURCE":    build.Spec.Source.Git.URI,
14
-	}
15
-	if build.Spec.Source.Git.Ref != "" {
16
-		envVars["OPENSHIFT_BUILD_REFERENCE"] = build.Spec.Source.Git.Ref
17
-	}
18
-	if build.Spec.Revision != nil &&
19
-		build.Spec.Revision.Git != nil &&
20
-		build.Spec.Revision.Git.Commit != "" {
21
-		envVars["OPENSHIFT_BUILD_COMMIT"] = build.Spec.Revision.Git.Commit
22
-	}
23
-	if build.Spec.Strategy.Type == buildapi.SourceBuildStrategyType {
24
-		userEnv := build.Spec.Strategy.SourceStrategy.Env
25
-		for _, v := range userEnv {
26
-			envVars[v.Name] = v.Value
27
-		}
28
-	}
29
-	return envVars
30
-}
31 1
deleted file mode 100644
... ...
@@ -1,84 +0,0 @@
1
-package builder
2
-
3
-import (
4
-	"testing"
5
-
6
-	"github.com/openshift/origin/pkg/build/api"
7
-	kapi "k8s.io/kubernetes/pkg/api"
8
-)
9
-
10
-func TestImageTag(t *testing.T) {
11
-	type tagTest struct {
12
-		build    api.Build
13
-		expected string
14
-	}
15
-	tests := []tagTest{
16
-		{
17
-			build: api.Build{
18
-				Spec: api.BuildSpec{
19
-					Output: api.BuildOutput{
20
-						To: &kapi.ObjectReference{
21
-							Kind: "DockerImage",
22
-							Name: "test/tag",
23
-						},
24
-					},
25
-				},
26
-			},
27
-			expected: "test/tag",
28
-		},
29
-		{
30
-			build: api.Build{
31
-				Spec: api.BuildSpec{
32
-					Output: api.BuildOutput{
33
-						To: &kapi.ObjectReference{
34
-							Kind: "DockerImage",
35
-							Name: "registry-server.test:5000/test/tag",
36
-						},
37
-					},
38
-				},
39
-			},
40
-			expected: "registry-server.test:5000/test/tag",
41
-		},
42
-	}
43
-	for _, x := range tests {
44
-		result := x.build.Spec.Output.To.Name
45
-		if result != x.expected {
46
-			t.Errorf("Unexpected imageTag result. Expected: %s, Actual: %s",
47
-				result, x.expected)
48
-		}
49
-	}
50
-}
51
-
52
-func TestGetBuildEnvVars(t *testing.T) {
53
-	b := &api.Build{
54
-		ObjectMeta: kapi.ObjectMeta{
55
-			Name: "1234",
56
-		},
57
-		Spec: api.BuildSpec{
58
-			Source: api.BuildSource{
59
-				Git: &api.GitBuildSource{
60
-					URI: "github.com/build/uri",
61
-					Ref: "my-branch",
62
-				},
63
-			},
64
-			Revision: &api.SourceRevision{
65
-				Git: &api.GitSourceRevision{
66
-					Commit: "56789",
67
-				},
68
-			},
69
-		},
70
-	}
71
-
72
-	vars := getBuildEnvVars(b)
73
-	expected := map[string]string{
74
-		"OPENSHIFT_BUILD_NAME":      "1234",
75
-		"OPENSHIFT_BUILD_SOURCE":    "github.com/build/uri",
76
-		"OPENSHIFT_BUILD_REFERENCE": "my-branch",
77
-		"OPENSHIFT_BUILD_COMMIT":    "56789",
78
-	}
79
-	for k, v := range expected {
80
-		if vars[k] != v {
81
-			t.Errorf("Expected: %s,%s, Got: %s,%s", k, v, k, vars[k])
82
-		}
83
-	}
84
-}
... ...
@@ -21,6 +21,18 @@ func Env(m []KeyValue) (string, error) {
21 21
 	return keyValueInstruction(command.Env, m)
22 22
 }
23 23
 
24
+// From builds a FROM Dockerfile instruction referring the base image image.
25
+func From(image string) (string, error) {
26
+	return unquotedArgsInstruction(command.From, image)
27
+}
28
+
29
+// Label builds a LABEL Dockerfile instruction from the mapping m. Keys and
30
+// values are serialized as JSON strings to ensure compatibility with the
31
+// Dockerfile parser.
32
+func Label(m []KeyValue) (string, error) {
33
+	return keyValueInstruction(command.Label, m)
34
+}
35
+
24 36
 // keyValueInstruction builds a Dockerfile instruction from the mapping m. Keys
25 37
 // and values are serialized as JSON strings to ensure compatibility with the
26 38
 // Dockerfile parser. Syntax:
... ...
@@ -42,3 +54,15 @@ func keyValueInstruction(cmd string, m []KeyValue) (string, error) {
42 42
 	}
43 43
 	return strings.Join(s, " "), nil
44 44
 }
45
+
46
+// unquotedArgsInstruction builds a Dockerfile instruction that takes unquoted
47
+// string arguments. Syntax:
48
+//   COMMAND single unquoted argument
49
+//   COMMAND value1 value2 value3 ...
50
+func unquotedArgsInstruction(cmd string, args ...string) (string, error) {
51
+	s := []string{strings.ToUpper(cmd)}
52
+	for _, arg := range args {
53
+		s = append(s, strings.Split(arg, "\n")...)
54
+	}
55
+	return strings.TrimRight(strings.Join(s, " "), " "), nil
56
+}
... ...
@@ -1,20 +1,34 @@
1 1
 package dockerfile
2 2
 
3
-import "testing"
3
+import (
4
+	"fmt"
5
+	"strings"
6
+	"testing"
4 7
 
5
-// TestEnv tests calling Env with multiple inputs.
6
-func TestEnv(t *testing.T) {
8
+	"github.com/docker/docker/builder/command"
9
+)
10
+
11
+// TestKeyValueInstructions tests calling derivatives of keyValueInstruction
12
+// with multiple inputs.
13
+func TestKeyValueInstructions(t *testing.T) {
14
+	keyValuesInstructions := []struct {
15
+		f   func([]KeyValue) (string, error)
16
+		cmd string
17
+	}{
18
+		{Env, command.Env},
19
+		{Label, command.Label},
20
+	}
7 21
 	testCases := []struct {
8 22
 		in   []KeyValue
9 23
 		want string
10 24
 	}{
11 25
 		{
12 26
 			in:   nil,
13
-			want: `ENV`,
27
+			want: ``,
14 28
 		},
15 29
 		{
16 30
 			in:   []KeyValue{},
17
-			want: `ENV`,
31
+			want: ``,
18 32
 		},
19 33
 		{
20 34
 			in: []KeyValue{
... ...
@@ -22,14 +36,14 @@ func TestEnv(t *testing.T) {
22 22
 				{"", "ABC"},
23 23
 				{"ABC", ""},
24 24
 			},
25
-			want: `ENV ""="" ""="ABC" "ABC"=""`,
25
+			want: `""="" ""="ABC" "ABC"=""`,
26 26
 		},
27 27
 		{
28 28
 			in: []KeyValue{
29 29
 				{"GOPATH", "/go"},
30 30
 				{"MSG", "Hello World!"},
31 31
 			},
32
-			want: `ENV "GOPATH"="/go" "MSG"="Hello World!"`,
32
+			want: `"GOPATH"="/go" "MSG"="Hello World!"`,
33 33
 		},
34 34
 		{
35 35
 			in: []KeyValue{
... ...
@@ -37,13 +51,13 @@ func TestEnv(t *testing.T) {
37 37
 				{"GOPATH", "/go"},
38 38
 				{"PATH", "$GOPATH/bin:$PATH"},
39 39
 			},
40
-			want: `ENV "PATH"="/bin" "GOPATH"="/go" "PATH"="$GOPATH/bin:$PATH"`,
40
+			want: `"PATH"="/bin" "GOPATH"="/go" "PATH"="$GOPATH/bin:$PATH"`,
41 41
 		},
42 42
 		{
43 43
 			in: []KeyValue{
44 44
 				{"你好", "我会说汉语"},
45 45
 			},
46
-			want: `ENV "你好"="我会说汉语"`,
46
+			want: `"你好"="我会说汉语"`,
47 47
 		},
48 48
 		{
49 49
 			// This tests handling an string encoding edge case.
... ...
@@ -51,16 +65,53 @@ func TestEnv(t *testing.T) {
51 51
 			in: []KeyValue{
52 52
 				{"☃", "'\" \\ / \b \f \n \r \t \x00"},
53 53
 			},
54
-			want: `ENV "☃"="'\" \\ / \u0008 \u000c \n \r \t \u0000"`,
54
+			want: `"☃"="'\" \\ / \u0008 \u000c \n \r \t \u0000"`,
55
+		},
56
+	}
57
+	for _, tc := range testCases {
58
+		for _, kvi := range keyValuesInstructions {
59
+			got, err := kvi.f(tc.in)
60
+			if err != nil {
61
+				t.Fatal(err)
62
+			}
63
+			want := strings.TrimRight(fmt.Sprintf("%s %s", strings.ToUpper(kvi.cmd), tc.want), " ")
64
+			if got != want {
65
+				t.Errorf("%s(%v) = %q; want %q", strings.Title(kvi.cmd), tc.in, got, want)
66
+			}
67
+		}
68
+	}
69
+}
70
+
71
+// TestFrom tests calling From with multiple inputs.
72
+func TestFrom(t *testing.T) {
73
+	testCases := []struct {
74
+		in   string
75
+		want string
76
+	}{
77
+		{
78
+			in:   "",
79
+			want: `FROM`,
80
+		},
81
+		{
82
+			in:   "centos:latest",
83
+			want: `FROM centos:latest`,
84
+		},
85
+		{
86
+			in:   "中关村",
87
+			want: `FROM 中关村`,
88
+		},
89
+		{
90
+			in:   "centos\nRUN rm -rf /\n\nUSER 100",
91
+			want: `FROM centos RUN rm -rf /  USER 100`,
55 92
 		},
56 93
 	}
57 94
 	for _, tc := range testCases {
58
-		got, err := Env(tc.in)
95
+		got, err := From(tc.in)
59 96
 		if err != nil {
60 97
 			t.Fatal(err)
61 98
 		}
62 99
 		if got != tc.want {
63
-			t.Errorf("Env(%v) = %q; want %q", tc.in, got, tc.want)
100
+			t.Errorf("From(%v) = %q; want %q", tc.in, got, tc.want)
64 101
 		}
65 102
 	}
66 103
 }