Browse code

builder: ONBUILD triggers were not accurately being executed in JSON cases.

Docker-DCO-1.1-Signed-off-by: Erik Hollensbe <github@hollensbe.org> (github: erikh)

Erik Hollensbe authored on 2014/10/14 05:14:35
Showing 5 changed files
... ...
@@ -20,7 +20,7 @@ import (
20 20
 )
21 21
 
22 22
 // dispatch with no layer / parsing. This is effectively not a command.
23
-func nullDispatch(b *Builder, args []string, attributes map[string]bool) error {
23
+func nullDispatch(b *Builder, args []string, attributes map[string]bool, original string) error {
24 24
 	return nil
25 25
 }
26 26
 
... ...
@@ -29,7 +29,7 @@ func nullDispatch(b *Builder, args []string, attributes map[string]bool) error {
29 29
 // Sets the environment variable foo to bar, also makes interpolation
30 30
 // in the dockerfile available from the next statement on via ${foo}.
31 31
 //
32
-func env(b *Builder, args []string, attributes map[string]bool) error {
32
+func env(b *Builder, args []string, attributes map[string]bool, original string) error {
33 33
 	if len(args) != 2 {
34 34
 		return fmt.Errorf("ENV accepts two arguments")
35 35
 	}
... ...
@@ -50,7 +50,7 @@ func env(b *Builder, args []string, attributes map[string]bool) error {
50 50
 // MAINTAINER some text <maybe@an.email.address>
51 51
 //
52 52
 // Sets the maintainer metadata.
53
-func maintainer(b *Builder, args []string, attributes map[string]bool) error {
53
+func maintainer(b *Builder, args []string, attributes map[string]bool, original string) error {
54 54
 	if len(args) != 1 {
55 55
 		return fmt.Errorf("MAINTAINER requires only one argument")
56 56
 	}
... ...
@@ -64,7 +64,7 @@ func maintainer(b *Builder, args []string, attributes map[string]bool) error {
64 64
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
65 65
 // exist here. If you do not wish to have this automatic handling, use COPY.
66 66
 //
67
-func add(b *Builder, args []string, attributes map[string]bool) error {
67
+func add(b *Builder, args []string, attributes map[string]bool, original string) error {
68 68
 	if len(args) < 2 {
69 69
 		return fmt.Errorf("ADD requires at least two arguments")
70 70
 	}
... ...
@@ -76,7 +76,7 @@ func add(b *Builder, args []string, attributes map[string]bool) error {
76 76
 //
77 77
 // Same as 'ADD' but without the tar and remote url handling.
78 78
 //
79
-func dispatchCopy(b *Builder, args []string, attributes map[string]bool) error {
79
+func dispatchCopy(b *Builder, args []string, attributes map[string]bool, original string) error {
80 80
 	if len(args) < 2 {
81 81
 		return fmt.Errorf("COPY requires at least two arguments")
82 82
 	}
... ...
@@ -88,7 +88,7 @@ func dispatchCopy(b *Builder, args []string, attributes map[string]bool) error {
88 88
 //
89 89
 // This sets the image the dockerfile will build on top of.
90 90
 //
91
-func from(b *Builder, args []string, attributes map[string]bool) error {
91
+func from(b *Builder, args []string, attributes map[string]bool, original string) error {
92 92
 	if len(args) != 1 {
93 93
 		return fmt.Errorf("FROM requires one argument")
94 94
 	}
... ...
@@ -120,7 +120,7 @@ func from(b *Builder, args []string, attributes map[string]bool) error {
120 120
 // special cases. search for 'OnBuild' in internals.go for additional special
121 121
 // cases.
122 122
 //
123
-func onbuild(b *Builder, args []string, attributes map[string]bool) error {
123
+func onbuild(b *Builder, args []string, attributes map[string]bool, original string) error {
124 124
 	triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0]))
125 125
 	switch triggerInstruction {
126 126
 	case "ONBUILD":
... ...
@@ -129,17 +129,17 @@ func onbuild(b *Builder, args []string, attributes map[string]bool) error {
129 129
 		return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
130 130
 	}
131 131
 
132
-	trigger := strings.Join(args, " ")
132
+	original = strings.TrimSpace(strings.TrimLeft(original, "ONBUILD"))
133 133
 
134
-	b.Config.OnBuild = append(b.Config.OnBuild, trigger)
135
-	return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", trigger))
134
+	b.Config.OnBuild = append(b.Config.OnBuild, original)
135
+	return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", original))
136 136
 }
137 137
 
138 138
 // WORKDIR /tmp
139 139
 //
140 140
 // Set the working directory for future RUN/CMD/etc statements.
141 141
 //
142
-func workdir(b *Builder, args []string, attributes map[string]bool) error {
142
+func workdir(b *Builder, args []string, attributes map[string]bool, original string) error {
143 143
 	if len(args) != 1 {
144 144
 		return fmt.Errorf("WORKDIR requires exactly one argument")
145 145
 	}
... ...
@@ -167,7 +167,7 @@ func workdir(b *Builder, args []string, attributes map[string]bool) error {
167 167
 // RUN echo hi          # sh -c echo hi
168 168
 // RUN [ "echo", "hi" ] # echo hi
169 169
 //
170
-func run(b *Builder, args []string, attributes map[string]bool) error {
170
+func run(b *Builder, args []string, attributes map[string]bool, original string) error {
171 171
 	if b.image == "" {
172 172
 		return fmt.Errorf("Please provide a source image with `from` prior to run")
173 173
 	}
... ...
@@ -230,7 +230,7 @@ func run(b *Builder, args []string, attributes map[string]bool) error {
230 230
 // Set the default command to run in the container (which may be empty).
231 231
 // Argument handling is the same as RUN.
232 232
 //
233
-func cmd(b *Builder, args []string, attributes map[string]bool) error {
233
+func cmd(b *Builder, args []string, attributes map[string]bool, original string) error {
234 234
 	b.Config.Cmd = handleJsonArgs(args, attributes)
235 235
 
236 236
 	if !attributes["json"] && len(b.Config.Entrypoint) == 0 {
... ...
@@ -256,7 +256,7 @@ func cmd(b *Builder, args []string, attributes map[string]bool) error {
256 256
 // Handles command processing similar to CMD and RUN, only b.Config.Entrypoint
257 257
 // is initialized at NewBuilder time instead of through argument parsing.
258 258
 //
259
-func entrypoint(b *Builder, args []string, attributes map[string]bool) error {
259
+func entrypoint(b *Builder, args []string, attributes map[string]bool, original string) error {
260 260
 	parsed := handleJsonArgs(args, attributes)
261 261
 
262 262
 	switch {
... ...
@@ -289,7 +289,7 @@ func entrypoint(b *Builder, args []string, attributes map[string]bool) error {
289 289
 // Expose ports for links and port mappings. This all ends up in
290 290
 // b.Config.ExposedPorts for runconfig.
291 291
 //
292
-func expose(b *Builder, args []string, attributes map[string]bool) error {
292
+func expose(b *Builder, args []string, attributes map[string]bool, original string) error {
293 293
 	portsTab := args
294 294
 
295 295
 	if b.Config.ExposedPorts == nil {
... ...
@@ -316,7 +316,7 @@ func expose(b *Builder, args []string, attributes map[string]bool) error {
316 316
 // Set the user to 'foo' for future commands and when running the
317 317
 // ENTRYPOINT/CMD at container run time.
318 318
 //
319
-func user(b *Builder, args []string, attributes map[string]bool) error {
319
+func user(b *Builder, args []string, attributes map[string]bool, original string) error {
320 320
 	if len(args) != 1 {
321 321
 		return fmt.Errorf("USER requires exactly one argument")
322 322
 	}
... ...
@@ -329,7 +329,7 @@ func user(b *Builder, args []string, attributes map[string]bool) error {
329 329
 //
330 330
 // Expose the volume /foo for use. Will also accept the JSON array form.
331 331
 //
332
-func volume(b *Builder, args []string, attributes map[string]bool) error {
332
+func volume(b *Builder, args []string, attributes map[string]bool, original string) error {
333 333
 	if len(args) == 0 {
334 334
 		return fmt.Errorf("Volume cannot be empty")
335 335
 	}
... ...
@@ -347,6 +347,6 @@ func volume(b *Builder, args []string, attributes map[string]bool) error {
347 347
 }
348 348
 
349 349
 // INSERT is no longer accepted, but we still parse it.
350
-func insert(b *Builder, args []string, attributes map[string]bool) error {
350
+func insert(b *Builder, args []string, attributes map[string]bool, original string) error {
351 351
 	return fmt.Errorf("INSERT has been deprecated. Please use ADD instead")
352 352
 }
... ...
@@ -41,10 +41,10 @@ var (
41 41
 	ErrDockerfileEmpty = errors.New("Dockerfile cannot be empty")
42 42
 )
43 43
 
44
-var evaluateTable map[string]func(*Builder, []string, map[string]bool) error
44
+var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error
45 45
 
46 46
 func init() {
47
-	evaluateTable = map[string]func(*Builder, []string, map[string]bool) error{
47
+	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
48 48
 		"env":        env,
49 49
 		"maintainer": maintainer,
50 50
 		"add":        add,
... ...
@@ -190,6 +190,7 @@ func (b *Builder) Run(context io.Reader) (string, error) {
190 190
 func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
191 191
 	cmd := ast.Value
192 192
 	attrs := ast.Attributes
193
+	original := ast.Original
193 194
 	strs := []string{}
194 195
 	msg := fmt.Sprintf("Step %d : %s", stepN, strings.ToUpper(cmd))
195 196
 
... ...
@@ -210,7 +211,7 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
210 210
 	// XXX yes, we skip any cmds that are not valid; the parser should have
211 211
 	// picked these out already.
212 212
 	if f, ok := evaluateTable[cmd]; ok {
213
-		return f(b, strs, attrs)
213
+		return f(b, strs, attrs, original)
214 214
 	}
215 215
 
216 216
 	fmt.Fprintf(b.ErrStream, "# Skipping unknown instruction %s\n", strings.ToUpper(cmd))
... ...
@@ -18,6 +18,7 @@ import (
18 18
 	"syscall"
19 19
 	"time"
20 20
 
21
+	"github.com/docker/docker/builder/parser"
21 22
 	"github.com/docker/docker/daemon"
22 23
 	imagepkg "github.com/docker/docker/image"
23 24
 	"github.com/docker/docker/pkg/archive"
... ...
@@ -436,30 +437,26 @@ func (b *Builder) processImageFrom(img *imagepkg.Image) error {
436 436
 	onBuildTriggers := b.Config.OnBuild
437 437
 	b.Config.OnBuild = []string{}
438 438
 
439
-	// FIXME rewrite this so that builder/parser is used; right now steps in
440
-	// onbuild are muted because we have no good way to represent the step
441
-	// number
439
+	// parse the ONBUILD triggers by invoking the parser
442 440
 	for stepN, step := range onBuildTriggers {
443
-		splitStep := strings.Split(step, " ")
444
-		stepInstruction := strings.ToUpper(strings.Trim(splitStep[0], " "))
445
-		switch stepInstruction {
446
-		case "ONBUILD":
447
-			return fmt.Errorf("Source image contains forbidden chained `ONBUILD ONBUILD` trigger: %s", step)
448
-		case "MAINTAINER", "FROM":
449
-			return fmt.Errorf("Source image contains forbidden %s trigger: %s", stepInstruction, step)
441
+		ast, err := parser.Parse(strings.NewReader(step))
442
+		if err != nil {
443
+			return err
450 444
 		}
451 445
 
452
-		// FIXME we have to run the evaluator manually here. This does not belong
453
-		// in this function. Once removed, the init() in evaluator.go should no
454
-		// longer be necessary.
446
+		for i, n := range ast.Children {
447
+			switch strings.ToUpper(n.Value) {
448
+			case "ONBUILD":
449
+				return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
450
+			case "MAINTAINER", "FROM":
451
+				return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value)
452
+			}
455 453
 
456
-		if f, ok := evaluateTable[strings.ToLower(stepInstruction)]; ok {
457 454
 			fmt.Fprintf(b.OutStream, "Trigger %d, %s\n", stepN, step)
458
-			if err := f(b, splitStep[1:], nil); err != nil {
455
+
456
+			if err := b.dispatch(i, n); err != nil {
459 457
 				return err
460 458
 			}
461
-		} else {
462
-			return fmt.Errorf("%s doesn't appear to be a valid Dockerfile instruction", splitStep[0])
463 459
 		}
464 460
 	}
465 461
 
... ...
@@ -26,6 +26,7 @@ type Node struct {
26 26
 	Next       *Node           // the next item in the current sexp
27 27
 	Children   []*Node         // the children of this sexp
28 28
 	Attributes map[string]bool // special attributes for this node
29
+	Original   string          // original line used before parsing
29 30
 }
30 31
 
31 32
 var (
... ...
@@ -84,6 +85,7 @@ func parseLine(line string) (string, *Node, error) {
84 84
 	if sexp.Value != "" || sexp.Next != nil || sexp.Children != nil {
85 85
 		node.Next = sexp
86 86
 		node.Attributes = attrs
87
+		node.Original = line
87 88
 	}
88 89
 
89 90
 	return "", node, nil
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"os"
8 8
 	"os/exec"
9 9
 	"path/filepath"
10
+	"regexp"
10 11
 	"strings"
11 12
 	"testing"
12 13
 	"time"
... ...
@@ -14,6 +15,165 @@ import (
14 14
 	"github.com/docker/docker/pkg/archive"
15 15
 )
16 16
 
17
+func TestBuildOnBuildForbiddenMaintainerInSourceImage(t *testing.T) {
18
+	name := "testbuildonbuildforbiddenmaintainerinsourceimage"
19
+	defer deleteImages(name)
20
+	createCmd := exec.Command(dockerBinary, "create", "busybox", "true")
21
+	out, _, _, err := runCommandWithStdoutStderr(createCmd)
22
+	errorOut(err, t, out)
23
+
24
+	cleanedContainerID := stripTrailingCharacters(out)
25
+
26
+	commitCmd := exec.Command(dockerBinary, "commit", "--run", "{\"OnBuild\":[\"MAINTAINER docker.io\"]}", cleanedContainerID, "onbuild")
27
+
28
+	if _, err := runCommand(commitCmd); err != nil {
29
+		t.Fatal(err)
30
+	}
31
+
32
+	_, err = buildImage(name,
33
+		`FROM onbuild`,
34
+		true)
35
+	if err != nil {
36
+		if !strings.Contains(err.Error(), "maintainer isn't allowed as an ONBUILD trigger") {
37
+			t.Fatalf("Wrong error %v, must be about MAINTAINER and ONBUILD in source image", err)
38
+		}
39
+	} else {
40
+		t.Fatal("Error must not be nil")
41
+	}
42
+	logDone("build - onbuild forbidden maintainer in source image")
43
+
44
+}
45
+
46
+func TestBuildOnBuildForbiddenFromInSourceImage(t *testing.T) {
47
+	name := "testbuildonbuildforbiddenfrominsourceimage"
48
+	defer deleteImages(name)
49
+	createCmd := exec.Command(dockerBinary, "create", "busybox", "true")
50
+	out, _, _, err := runCommandWithStdoutStderr(createCmd)
51
+	errorOut(err, t, out)
52
+
53
+	cleanedContainerID := stripTrailingCharacters(out)
54
+
55
+	commitCmd := exec.Command(dockerBinary, "commit", "--run", "{\"OnBuild\":[\"FROM busybox\"]}", cleanedContainerID, "onbuild")
56
+
57
+	if _, err := runCommand(commitCmd); err != nil {
58
+		t.Fatal(err)
59
+	}
60
+
61
+	_, err = buildImage(name,
62
+		`FROM onbuild`,
63
+		true)
64
+	if err != nil {
65
+		if !strings.Contains(err.Error(), "from isn't allowed as an ONBUILD trigger") {
66
+			t.Fatalf("Wrong error %v, must be about FROM and ONBUILD in source image", err)
67
+		}
68
+	} else {
69
+		t.Fatal("Error must not be nil")
70
+	}
71
+	logDone("build - onbuild forbidden from in source image")
72
+
73
+}
74
+
75
+func TestBuildOnBuildForbiddenChainedInSourceImage(t *testing.T) {
76
+	name := "testbuildonbuildforbiddenchainedinsourceimage"
77
+	defer deleteImages(name)
78
+	createCmd := exec.Command(dockerBinary, "create", "busybox", "true")
79
+	out, _, _, err := runCommandWithStdoutStderr(createCmd)
80
+	errorOut(err, t, out)
81
+
82
+	cleanedContainerID := stripTrailingCharacters(out)
83
+
84
+	commitCmd := exec.Command(dockerBinary, "commit", "--run", "{\"OnBuild\":[\"ONBUILD RUN ls\"]}", cleanedContainerID, "onbuild")
85
+
86
+	if _, err := runCommand(commitCmd); err != nil {
87
+		t.Fatal(err)
88
+	}
89
+
90
+	_, err = buildImage(name,
91
+		`FROM onbuild`,
92
+		true)
93
+	if err != nil {
94
+		if !strings.Contains(err.Error(), "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") {
95
+			t.Fatalf("Wrong error %v, must be about chaining ONBUILD in source image", err)
96
+		}
97
+	} else {
98
+		t.Fatal("Error must not be nil")
99
+	}
100
+	logDone("build - onbuild forbidden chained in source image")
101
+
102
+}
103
+
104
+func TestBuildOnBuildCmdEntrypointJSON(t *testing.T) {
105
+	name1 := "onbuildcmd"
106
+	name2 := "onbuildgenerated"
107
+
108
+	defer deleteAllContainers()
109
+	defer deleteImages(name2)
110
+	defer deleteImages(name1)
111
+
112
+	_, err := buildImage(name1, `
113
+FROM busybox
114
+ONBUILD CMD ["hello world"]
115
+ONBUILD ENTRYPOINT ["echo"]
116
+ONBUILD RUN ["true"]`,
117
+		false)
118
+
119
+	if err != nil {
120
+		t.Fatal(err)
121
+	}
122
+
123
+	_, err = buildImage(name2, fmt.Sprintf(`FROM %s`, name1), false)
124
+
125
+	if err != nil {
126
+		t.Fatal(err)
127
+	}
128
+
129
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-t", name2))
130
+	if err != nil {
131
+		t.Fatal(err)
132
+	}
133
+
134
+	if !regexp.MustCompile(`(?m)^hello world`).MatchString(out) {
135
+		t.Fatal("did not get echo output from onbuild", out)
136
+	}
137
+
138
+	logDone("build - onbuild with json entrypoint/cmd")
139
+}
140
+
141
+func TestBuildOnBuildEntrypointJSON(t *testing.T) {
142
+	name1 := "onbuildcmd"
143
+	name2 := "onbuildgenerated"
144
+
145
+	defer deleteAllContainers()
146
+	defer deleteImages(name2)
147
+	defer deleteImages(name1)
148
+
149
+	_, err := buildImage(name1, `
150
+FROM busybox
151
+ONBUILD ENTRYPOINT ["echo"]`,
152
+		false)
153
+
154
+	if err != nil {
155
+		t.Fatal(err)
156
+	}
157
+
158
+	_, err = buildImage(name2, fmt.Sprintf("FROM %s\nCMD [\"hello world\"]\n", name1), false)
159
+
160
+	if err != nil {
161
+		t.Fatal(err)
162
+	}
163
+
164
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-t", name2))
165
+	if err != nil {
166
+		t.Fatal(err)
167
+	}
168
+
169
+	if !regexp.MustCompile(`(?m)^hello world`).MatchString(out) {
170
+		t.Fatal("got malformed output from onbuild", out)
171
+	}
172
+
173
+	logDone("build - onbuild with json entrypoint")
174
+}
175
+
17 176
 func TestBuildCacheADD(t *testing.T) {
18 177
 	name := "testbuildtwoimageswithadd"
19 178
 	defer deleteImages(name)
... ...
@@ -2386,8 +2546,8 @@ func TestBuildOnBuildOutput(t *testing.T) {
2386 2386
 		t.Fatal(err)
2387 2387
 	}
2388 2388
 
2389
-	if !strings.Contains(out, "Trigger 0, run echo foo") {
2390
-		t.Fatal("failed to find the ONBUILD output")
2389
+	if !strings.Contains(out, "Trigger 0, RUN echo foo") {
2390
+		t.Fatal("failed to find the ONBUILD output", out)
2391 2391
 	}
2392 2392
 
2393 2393
 	logDone("build - onbuild output")