Browse code

Pass env vars defined in Docker build strategy

Rodolfo Carvalho authored on 2015/09/18 00:57:17
Showing 8 changed files
... ...
@@ -21,6 +21,7 @@ import (
21 21
 
22 22
 	"github.com/openshift/origin/pkg/build/api"
23 23
 	"github.com/openshift/origin/pkg/build/builder/cmd/dockercfg"
24
+	"github.com/openshift/origin/pkg/util/docker/dockerfile"
24 25
 	s2iapi "github.com/openshift/source-to-image/pkg/api"
25 26
 	"github.com/openshift/source-to-image/pkg/scm/git"
26 27
 	"github.com/openshift/source-to-image/pkg/tar"
... ...
@@ -260,11 +261,20 @@ func (d *DockerBuilder) addBuildParameters(dir string) error {
260 260
 	}
261 261
 	labels = util.GenerateLabelsFromSourceInfo(labels, sourceInfo, api.DefaultDockerLabelNamespace)
262 262
 	newFileData = appendMetadata(Label, newFileData, labels)
263
-	if ioutil.WriteFile(dockerfilePath, []byte(newFileData), filePerm); err != nil {
263
+
264
+	node, err := parser.Parse(strings.NewReader(newFileData))
265
+	if err != nil {
264 266
 		return err
265 267
 	}
266 268
 
267
-	return nil
269
+	err = insertEnvAfterFrom(node, d.build.Spec.Strategy.DockerStrategy.Env)
270
+	if err != nil {
271
+		return err
272
+	}
273
+
274
+	instructions := dockerfile.ParseTreeToDockerfile(node)
275
+
276
+	return ioutil.WriteFile(dockerfilePath, instructions, filePerm)
268 277
 }
269 278
 
270 279
 // appendMetadata appends a Docker instruction that adds metadata values
... ...
@@ -416,6 +426,37 @@ func traverseAST(cmd string, node *parser.Node) int {
416 416
 	return index
417 417
 }
418 418
 
419
+// insertEnvAfterFrom inserts an ENV instruction with the environment variables
420
+// from env after every FROM instruction in node.
421
+func insertEnvAfterFrom(node *parser.Node, env []kapi.EnvVar) error {
422
+	if node == nil || len(env) == 0 {
423
+		return nil
424
+	}
425
+
426
+	// Build ENV instruction.
427
+	var m []dockerfile.KeyValue
428
+	for _, e := range env {
429
+		m = append(m, dockerfile.KeyValue{Key: e.Name, Value: e.Value})
430
+	}
431
+	buildEnv, err := dockerfile.Env(m)
432
+	if err != nil {
433
+		return err
434
+	}
435
+
436
+	// Insert the buildEnv after every FROM instruction.
437
+	// We iterate in reverse order, otherwise indices would have to be
438
+	// recomputed after each step, because we're changing node in-place.
439
+	indices := dockerfile.FindAll(node, dockercmd.From)
440
+	for i := len(indices) - 1; i >= 0; i-- {
441
+		err := dockerfile.InsertInstructions(node, indices[i]+1, buildEnv)
442
+		if err != nil {
443
+			return err
444
+		}
445
+	}
446
+
447
+	return nil
448
+}
449
+
419 450
 // setupPullSecret provides a Docker authentication configuration when the
420 451
 // PullSecret is specified.
421 452
 func (d *DockerBuilder) setupPullSecret() (*docker.AuthConfigurations, error) {
... ...
@@ -3,10 +3,15 @@ package builder
3 3
 import (
4 4
 	"bytes"
5 5
 	"log"
6
+	"reflect"
7
+	"strings"
6 8
 	"testing"
7 9
 
8 10
 	dockercmd "github.com/docker/docker/builder/command"
9 11
 	"github.com/docker/docker/builder/parser"
12
+	kapi "k8s.io/kubernetes/pkg/api"
13
+
14
+	"github.com/openshift/origin/pkg/util/docker/dockerfile"
10 15
 )
11 16
 
12 17
 func TestReplaceValidCmd(t *testing.T) {
... ...
@@ -249,6 +254,73 @@ func TestAppendLabels(t *testing.T) {
249 249
 	}
250 250
 }
251 251
 
252
+func TestInsertEnvAfterFrom(t *testing.T) {
253
+	tests := map[string]struct {
254
+		original string
255
+		env      []kapi.EnvVar
256
+		want     string
257
+	}{
258
+		"no FROM instruction": {
259
+			original: `RUN echo "invalid Dockerfile"
260
+`,
261
+			env: []kapi.EnvVar{
262
+				{Name: "PATH", Value: "/bin"},
263
+			},
264
+			want: `RUN echo "invalid Dockerfile"
265
+`},
266
+		"empty env": {
267
+			original: `FROM busybox
268
+`,
269
+			env: []kapi.EnvVar{},
270
+			want: `FROM busybox
271
+`},
272
+		"single FROM instruction": {
273
+			original: `FROM busybox
274
+RUN echo "hello world"
275
+`,
276
+			env: []kapi.EnvVar{
277
+				{Name: "PATH", Value: "/bin"},
278
+			},
279
+			want: `FROM busybox
280
+ENV "PATH"="/bin"
281
+RUN echo "hello world"
282
+`},
283
+		"multiple FROM instructions": {
284
+			original: `FROM scratch
285
+FROM busybox
286
+RUN echo "hello world"
287
+`,
288
+			env: []kapi.EnvVar{
289
+				{Name: "PATH", Value: "/bin"},
290
+				{Name: "GOPATH", Value: "/go"},
291
+				{Name: "PATH", Value: "/go/bin:$PATH"},
292
+			},
293
+			want: `FROM scratch
294
+ENV "PATH"="/bin" "GOPATH"="/go" "PATH"="/go/bin:$PATH"
295
+FROM busybox
296
+ENV "PATH"="/bin" "GOPATH"="/go" "PATH"="/go/bin:$PATH"
297
+RUN echo "hello world"
298
+`},
299
+	}
300
+	for name, test := range tests {
301
+		got, err := parser.Parse(strings.NewReader(test.original))
302
+		if err != nil {
303
+			t.Errorf("%s: %v", name, err)
304
+			continue
305
+		}
306
+		want, err := parser.Parse(strings.NewReader(test.want))
307
+		if err != nil {
308
+			t.Errorf("%s: %v", name, err)
309
+			continue
310
+		}
311
+		insertEnvAfterFrom(got, test.env)
312
+		if !reflect.DeepEqual(got, want) {
313
+			t.Errorf("%s: insertEnvAfterFrom(node, %+v) = %+v; want %+v", name, test.env, got, want)
314
+			t.Logf("resulting Dockerfile:\n%s", dockerfile.ParseTreeToDockerfile(got))
315
+		}
316
+	}
317
+}
318
+
252 319
 const (
253 320
 	dockerFile = `
254 321
 FROM openshift/origin-base
255 322
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+// Package docker has utilities to work with Docker.
1
+package docker
0 2
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+// Package dockerfile has utilities that complement Docker's official Dockerfile
1
+// parser.
2
+package dockerfile
0 3
new file mode 100644
... ...
@@ -0,0 +1,64 @@
0
+package dockerfile
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/docker/docker/builder/parser"
7
+)
8
+
9
+// ParseTreeToDockerfile takes a Dockerfile AST node, as generated by
10
+// parser.Parse, and returns a new, equivalent, Dockerfile.
11
+func ParseTreeToDockerfile(node *parser.Node) []byte {
12
+	if node == nil {
13
+		return nil
14
+	}
15
+	buf := []byte(node.Original)
16
+	for _, child := range node.Children {
17
+		buf = append(buf, ParseTreeToDockerfile(child)...)
18
+	}
19
+	// Append a line break when needed.
20
+	if len(buf) > 0 && buf[len(buf)-1] != '\n' {
21
+		buf = append(buf, '\n')
22
+	}
23
+	return buf
24
+}
25
+
26
+// FindAll returns the indices of all children of node such that
27
+// node.Children[i].Value == cmd. Valid values for cmd are defined in the
28
+// package github.com/docker/docker/builder/command.
29
+func FindAll(node *parser.Node, cmd string) []int {
30
+	if node == nil {
31
+		return nil
32
+	}
33
+	var indices []int
34
+	for i, child := range node.Children {
35
+		if child != nil && child.Value == cmd {
36
+			indices = append(indices, i)
37
+		}
38
+	}
39
+	return indices
40
+}
41
+
42
+// InsertInstructions inserts instructions starting from the pos-th child of
43
+// node, moving other children as necessary. The instructions should be valid
44
+// Dockerfile instructions. InsertInstructions mutates node in-place, and the
45
+// final state of node is equivalent to what parser.Parse would return if the
46
+// original Dockerfile represented by node contained the instructions at the
47
+// specified position pos. If the returned error is non-nil, node is guaranteed
48
+// to be unchanged.
49
+func InsertInstructions(node *parser.Node, pos int, instructions string) error {
50
+	if node == nil {
51
+		return fmt.Errorf("cannot insert instructions in a nil node")
52
+	}
53
+	if pos < 0 || pos > len(node.Children) {
54
+		return fmt.Errorf("pos %d out of range [0, %d]", pos, len(node.Children)-1)
55
+	}
56
+	newChild, err := parser.Parse(strings.NewReader(instructions))
57
+	if err != nil {
58
+		return err
59
+	}
60
+	// InsertVector pattern (https://github.com/golang/go/wiki/SliceTricks)
61
+	node.Children = append(node.Children[:pos], append(newChild.Children, node.Children[pos:]...)...)
62
+	return nil
63
+}
0 64
new file mode 100644
... ...
@@ -0,0 +1,275 @@
0
+package dockerfile
1
+
2
+import (
3
+	"reflect"
4
+	"strings"
5
+	"testing"
6
+
7
+	"github.com/docker/docker/builder/command"
8
+	"github.com/docker/docker/builder/parser"
9
+)
10
+
11
+// TestParseTreeToDockerfile tests calling ParseTreeToDockerfile with multiple
12
+// valid inputs.
13
+func TestParseTreeToDockerfile(t *testing.T) {
14
+	testCases := map[string]struct {
15
+		in   string
16
+		want string
17
+	}{
18
+		"empty input": {
19
+			in:   ``,
20
+			want: ``,
21
+		},
22
+		"only comments": {
23
+			in: `# This is a comment
24
+# and this is another comment
25
+	# while this is an indented comment`,
26
+			want: ``,
27
+		},
28
+		"simple Dockerfile": {
29
+			in: `FROM scratch
30
+LABEL version=1.0
31
+FROM busybox
32
+ENV PATH=/bin
33
+`,
34
+			want: `FROM scratch
35
+LABEL version=1.0
36
+FROM busybox
37
+ENV PATH=/bin
38
+`,
39
+		},
40
+		"Dockerfile with comments": {
41
+			in: `# This is a Dockerfile
42
+FROM scratch
43
+LABEL version=1.0
44
+# Here we start building a second image
45
+FROM busybox
46
+ENV PATH=/bin
47
+`,
48
+			want: `FROM scratch
49
+LABEL version=1.0
50
+FROM busybox
51
+ENV PATH=/bin
52
+`,
53
+		},
54
+		"all Dockerfile instructions": {
55
+			in: `FROM busybox:latest
56
+MAINTAINER nobody@example.com
57
+ONBUILD ADD . /app/src
58
+ONBUILD RUN echo "Hello universe!"
59
+LABEL version=1.0
60
+EXPOSE 8080
61
+VOLUME /var/run/www
62
+ENV PATH=/bin
63
+ADD file /home/
64
+COPY dir/ /tmp/
65
+RUN echo "Hello world!"
66
+ENTRYPOINT /bin/sh
67
+CMD ["-c", "env"]
68
+USER 1001
69
+WORKDIR /home
70
+`,
71
+			want: `FROM busybox:latest
72
+MAINTAINER nobody@example.com
73
+ONBUILD ADD . /app/src
74
+ONBUILD RUN echo "Hello universe!"
75
+LABEL version=1.0
76
+EXPOSE 8080
77
+VOLUME /var/run/www
78
+ENV PATH=/bin
79
+ADD file /home/
80
+COPY dir/ /tmp/
81
+RUN echo "Hello world!"
82
+ENTRYPOINT /bin/sh
83
+CMD ["-c", "env"]
84
+USER 1001
85
+WORKDIR /home
86
+`,
87
+		},
88
+	}
89
+	for name, tc := range testCases {
90
+		node, err := parser.Parse(strings.NewReader(tc.in))
91
+		if err != nil {
92
+			t.Errorf("%s: parse error: %v", name, err)
93
+			continue
94
+		}
95
+		got := ParseTreeToDockerfile(node)
96
+		want := []byte(tc.want)
97
+		if !reflect.DeepEqual(got, want) {
98
+			t.Errorf("ParseTreeToDockerfile: %s:\ngot:\n%swant:\n%s", name, got, want)
99
+		}
100
+	}
101
+}
102
+
103
+// TestParseTreeToDockerfileNilNode tests calling ParseTreeToDockerfile with a
104
+// nil *parser.Node.
105
+func TestParseTreeToDockerfileNilNode(t *testing.T) {
106
+	got := ParseTreeToDockerfile(nil)
107
+	if got != nil {
108
+		t.Errorf("ParseTreeToDockerfile(nil) = %#v; want nil", got)
109
+	}
110
+}
111
+
112
+// TestFindAll tests calling FindAll with multiple values of cmd.
113
+func TestFindAll(t *testing.T) {
114
+	instructions := `FROM scratch
115
+LABEL version=1.0
116
+FROM busybox
117
+ENV PATH=/bin
118
+`
119
+	node, err := parser.Parse(strings.NewReader(instructions))
120
+	if err != nil {
121
+		t.Fatalf("parse error: %v", err)
122
+	}
123
+	for cmd, want := range map[string][]int{
124
+		command.From:       {0, 2},
125
+		command.Label:      {1},
126
+		command.Env:        {3},
127
+		command.Maintainer: nil,
128
+		"UnknownCommand":   nil,
129
+	} {
130
+		got := FindAll(node, cmd)
131
+		if !reflect.DeepEqual(got, want) {
132
+			t.Errorf("FindAll(node, %q) = %#v; want %#v", cmd, got, want)
133
+		}
134
+	}
135
+}
136
+
137
+// TestFindAllNilNode tests calling FindAll with a nil *parser.Node.
138
+func TestFindAllNilNode(t *testing.T) {
139
+	cmd := command.From
140
+	got := FindAll(nil, cmd)
141
+	if got != nil {
142
+		t.Errorf("FindAll(nil, %q) = %#v; want nil", cmd, got)
143
+	}
144
+}
145
+
146
+// TestInsertInstructions tests calling InsertInstructions with multiple valid
147
+// combinations of input.
148
+func TestInsertInstructions(t *testing.T) {
149
+	testCases := map[string]struct {
150
+		original        string
151
+		index           int
152
+		newInstructions string
153
+		want            string
154
+	}{
155
+		"insert nothing": {
156
+			original: `FROM busybox
157
+ENV PATH=/bin
158
+`,
159
+			index:           0,
160
+			newInstructions: ``,
161
+			want: `FROM busybox
162
+ENV PATH=/bin
163
+`,
164
+		},
165
+		"insert instruction in empty file": {
166
+			original:        ``,
167
+			index:           0,
168
+			newInstructions: `FROM busybox`,
169
+			want: `FROM busybox
170
+`,
171
+		},
172
+		"prepend single instruction": {
173
+			original: `FROM busybox
174
+ENV PATH=/bin
175
+`,
176
+			index:           0,
177
+			newInstructions: `FROM scratch`,
178
+			want: `FROM scratch
179
+FROM busybox
180
+ENV PATH=/bin
181
+`,
182
+		},
183
+		"append single instruction": {
184
+			original: `FROM busybox
185
+ENV PATH=/bin
186
+`,
187
+			index:           2,
188
+			newInstructions: `FROM scratch`,
189
+			want: `FROM busybox
190
+ENV PATH=/bin
191
+FROM scratch
192
+`,
193
+		},
194
+		"insert single instruction in the middle": {
195
+			original: `FROM busybox
196
+ENV PATH=/bin
197
+`,
198
+			index:           1,
199
+			newInstructions: `LABEL version=1.0`,
200
+			want: `FROM busybox
201
+LABEL version=1.0
202
+ENV PATH=/bin
203
+`,
204
+		},
205
+	}
206
+	for name, tc := range testCases {
207
+		got, err := parser.Parse(strings.NewReader(tc.original))
208
+		if err != nil {
209
+			t.Errorf("InsertInstructions: %s: parse error: %v", name, err)
210
+			continue
211
+		}
212
+		err = InsertInstructions(got, tc.index, tc.newInstructions)
213
+		if err != nil {
214
+			t.Errorf("InsertInstructions: %s: %v", name, err)
215
+			continue
216
+		}
217
+		want, err := parser.Parse(strings.NewReader(tc.want))
218
+		if err != nil {
219
+			t.Errorf("InsertInstructions: %s: parse error: %v", name, err)
220
+			continue
221
+		}
222
+		if !reflect.DeepEqual(got, want) {
223
+			t.Errorf("InsertInstructions: %s: got %#v; want %#v", name, got, want)
224
+		}
225
+	}
226
+}
227
+
228
+// TestInsertInstructionsNilNode tests calling InsertInstructions with a nil
229
+// *parser.Node.
230
+func TestInsertInstructionsNilNode(t *testing.T) {
231
+	err := InsertInstructions(nil, 0, "")
232
+	if err == nil {
233
+		t.Errorf("InsertInstructions: got nil; want error")
234
+	}
235
+}
236
+
237
+// TestInsertInstructionsPosOutOfRange tests calling InsertInstructions with
238
+// invalid values for the pos argument.
239
+func TestInsertInstructionsPosOutOfRange(t *testing.T) {
240
+	original := `FROM busybox
241
+ENV PATH=/bin
242
+`
243
+	node, err := parser.Parse(strings.NewReader(original))
244
+	if err != nil {
245
+		t.Fatalf("parse error: %v", err)
246
+	}
247
+	for _, pos := range []int{-1, 3, 4} {
248
+		err := InsertInstructions(node, pos, "")
249
+		if err == nil {
250
+			t.Errorf("InsertInstructions(node, %d, \"\"): got nil; want error", pos)
251
+		}
252
+	}
253
+}
254
+
255
+// TestInsertInstructionsUnparseable tests calling InsertInstructions with
256
+// instructions that the Docker parser cannot handle.
257
+func TestInsertInstructionsUnparseable(t *testing.T) {
258
+	original := `FROM busybox
259
+ENV PATH=/bin
260
+`
261
+	node, err := parser.Parse(strings.NewReader(original))
262
+	if err != nil {
263
+		t.Fatalf("parse error: %v", err)
264
+	}
265
+	for name, instructions := range map[string]string{
266
+		"env without value": `ENV PATH`,
267
+		"nested json":       `CMD [ "echo", [ "nested json" ] ]`,
268
+	} {
269
+		err = InsertInstructions(node, 1, instructions)
270
+		if err == nil {
271
+			t.Errorf("InsertInstructions: %s: got nil; want error", name)
272
+		}
273
+	}
274
+}
0 275
new file mode 100644
... ...
@@ -0,0 +1,44 @@
0
+package dockerfile
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"strings"
6
+
7
+	"github.com/docker/docker/builder/command"
8
+)
9
+
10
+// A KeyValue can be used to build ordered lists of key-value pairs.
11
+type KeyValue struct {
12
+	Key   string
13
+	Value string
14
+}
15
+
16
+// Env builds an ENV Dockerfile instruction from the mapping m. Keys and values
17
+// are serialized as JSON strings to ensure compatibility with the Dockerfile
18
+// parser.
19
+func Env(m []KeyValue) (string, error) {
20
+	return keyValueInstruction(command.Env, m)
21
+}
22
+
23
+// keyValueInstruction builds a Dockerfile instruction from the mapping m. Keys
24
+// and values are serialized as JSON strings to ensure compatibility with the
25
+// Dockerfile parser. Syntax:
26
+//   COMMAND "KEY"="VALUE" "may"="contain spaces"
27
+func keyValueInstruction(cmd string, m []KeyValue) (string, error) {
28
+	s := []string{strings.ToUpper(cmd)}
29
+	for _, kv := range m {
30
+		// Marshal kv.Key and kv.Value as JSON strings to cover cases
31
+		// like when the values contain spaces or special characters.
32
+		k, err := json.Marshal(kv.Key)
33
+		if err != nil {
34
+			return "", err
35
+		}
36
+		v, err := json.Marshal(kv.Value)
37
+		if err != nil {
38
+			return "", err
39
+		}
40
+		s = append(s, fmt.Sprintf("%s=%s", k, v))
41
+	}
42
+	return strings.Join(s, " "), nil
43
+}
0 44
new file mode 100644
... ...
@@ -0,0 +1,66 @@
0
+package dockerfile
1
+
2
+import "testing"
3
+
4
+// TestEnv tests calling Env with multiple inputs.
5
+func TestEnv(t *testing.T) {
6
+	testCases := []struct {
7
+		in   []KeyValue
8
+		want string
9
+	}{
10
+		{
11
+			in:   nil,
12
+			want: `ENV`,
13
+		},
14
+		{
15
+			in:   []KeyValue{},
16
+			want: `ENV`,
17
+		},
18
+		{
19
+			in: []KeyValue{
20
+				{"", ""},
21
+				{"", "ABC"},
22
+				{"ABC", ""},
23
+			},
24
+			want: `ENV ""="" ""="ABC" "ABC"=""`,
25
+		},
26
+		{
27
+			in: []KeyValue{
28
+				{"GOPATH", "/go"},
29
+				{"MSG", "Hello World!"},
30
+			},
31
+			want: `ENV "GOPATH"="/go" "MSG"="Hello World!"`,
32
+		},
33
+		{
34
+			in: []KeyValue{
35
+				{"PATH", "/bin"},
36
+				{"GOPATH", "/go"},
37
+				{"PATH", "$GOPATH/bin:$PATH"},
38
+			},
39
+			want: `ENV "PATH"="/bin" "GOPATH"="/go" "PATH"="$GOPATH/bin:$PATH"`,
40
+		},
41
+		{
42
+			in: []KeyValue{
43
+				{"你好", "我会说汉语"},
44
+			},
45
+			want: `ENV "你好"="我会说汉语"`,
46
+		},
47
+		{
48
+			// This tests handling an string encoding edge case.
49
+			// Example input taken from Docker parser's test suite.
50
+			in: []KeyValue{
51
+				{"☃", "'\" \\ / \b \f \n \r \t \x00"},
52
+			},
53
+			want: `ENV "☃"="'\" \\ / \u0008 \u000c \n \r \t \u0000"`,
54
+		},
55
+	}
56
+	for _, tc := range testCases {
57
+		got, err := Env(tc.in)
58
+		if err != nil {
59
+			t.Fatal(err)
60
+		}
61
+		if got != tc.want {
62
+			t.Errorf("Env(%v) = %q; want %q", tc.in, got, tc.want)
63
+		}
64
+	}
65
+}