瀏覽代碼

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 10 年之前
父節點
當前提交
1150c1639a
共有 5 個文件被更改,包括 200 次插入40 次删除
  1. 18 18
      builder/dispatchers.go
  2. 4 3
      builder/evaluator.go
  3. 14 17
      builder/internals.go
  4. 2 0
      builder/parser/parser.go
  5. 162 2
      integration-cli/docker_cli_build_test.go

+ 18 - 18
builder/dispatchers.go

@@ -20,7 +20,7 @@ import (
 )
 )
 
 
 // dispatch with no layer / parsing. This is effectively not a command.
 // dispatch with no layer / parsing. This is effectively not a command.
-func nullDispatch(b *Builder, args []string, attributes map[string]bool) error {
+func nullDispatch(b *Builder, args []string, attributes map[string]bool, original string) error {
 	return nil
 	return nil
 }
 }
 
 
@@ -29,7 +29,7 @@ func nullDispatch(b *Builder, args []string, attributes map[string]bool) error {
 // Sets the environment variable foo to bar, also makes interpolation
 // Sets the environment variable foo to bar, also makes interpolation
 // in the dockerfile available from the next statement on via ${foo}.
 // in the dockerfile available from the next statement on via ${foo}.
 //
 //
-func env(b *Builder, args []string, attributes map[string]bool) error {
+func env(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) != 2 {
 	if len(args) != 2 {
 		return fmt.Errorf("ENV accepts two arguments")
 		return fmt.Errorf("ENV accepts two arguments")
 	}
 	}
@@ -50,7 +50,7 @@ func env(b *Builder, args []string, attributes map[string]bool) error {
 // MAINTAINER some text <maybe@an.email.address>
 // MAINTAINER some text <maybe@an.email.address>
 //
 //
 // Sets the maintainer metadata.
 // Sets the maintainer metadata.
-func maintainer(b *Builder, args []string, attributes map[string]bool) error {
+func maintainer(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) != 1 {
 	if len(args) != 1 {
 		return fmt.Errorf("MAINTAINER requires only one argument")
 		return fmt.Errorf("MAINTAINER requires only one argument")
 	}
 	}
@@ -64,7 +64,7 @@ func maintainer(b *Builder, args []string, attributes map[string]bool) error {
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
 // exist here. If you do not wish to have this automatic handling, use COPY.
 // exist here. If you do not wish to have this automatic handling, use COPY.
 //
 //
-func add(b *Builder, args []string, attributes map[string]bool) error {
+func add(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) < 2 {
 	if len(args) < 2 {
 		return fmt.Errorf("ADD requires at least two arguments")
 		return fmt.Errorf("ADD requires at least two arguments")
 	}
 	}
@@ -76,7 +76,7 @@ func add(b *Builder, args []string, attributes map[string]bool) error {
 //
 //
 // Same as 'ADD' but without the tar and remote url handling.
 // Same as 'ADD' but without the tar and remote url handling.
 //
 //
-func dispatchCopy(b *Builder, args []string, attributes map[string]bool) error {
+func dispatchCopy(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) < 2 {
 	if len(args) < 2 {
 		return fmt.Errorf("COPY requires at least two arguments")
 		return fmt.Errorf("COPY requires at least two arguments")
 	}
 	}
@@ -88,7 +88,7 @@ func dispatchCopy(b *Builder, args []string, attributes map[string]bool) error {
 //
 //
 // This sets the image the dockerfile will build on top of.
 // This sets the image the dockerfile will build on top of.
 //
 //
-func from(b *Builder, args []string, attributes map[string]bool) error {
+func from(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) != 1 {
 	if len(args) != 1 {
 		return fmt.Errorf("FROM requires one argument")
 		return fmt.Errorf("FROM requires one argument")
 	}
 	}
@@ -120,7 +120,7 @@ func from(b *Builder, args []string, attributes map[string]bool) error {
 // special cases. search for 'OnBuild' in internals.go for additional special
 // special cases. search for 'OnBuild' in internals.go for additional special
 // cases.
 // cases.
 //
 //
-func onbuild(b *Builder, args []string, attributes map[string]bool) error {
+func onbuild(b *Builder, args []string, attributes map[string]bool, original string) error {
 	triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0]))
 	triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0]))
 	switch triggerInstruction {
 	switch triggerInstruction {
 	case "ONBUILD":
 	case "ONBUILD":
@@ -129,17 +129,17 @@ func onbuild(b *Builder, args []string, attributes map[string]bool) error {
 		return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
 		return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
 	}
 	}
 
 
-	trigger := strings.Join(args, " ")
+	original = strings.TrimSpace(strings.TrimLeft(original, "ONBUILD"))
 
 
-	b.Config.OnBuild = append(b.Config.OnBuild, trigger)
-	return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", trigger))
+	b.Config.OnBuild = append(b.Config.OnBuild, original)
+	return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", original))
 }
 }
 
 
 // WORKDIR /tmp
 // WORKDIR /tmp
 //
 //
 // Set the working directory for future RUN/CMD/etc statements.
 // Set the working directory for future RUN/CMD/etc statements.
 //
 //
-func workdir(b *Builder, args []string, attributes map[string]bool) error {
+func workdir(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) != 1 {
 	if len(args) != 1 {
 		return fmt.Errorf("WORKDIR requires exactly one argument")
 		return fmt.Errorf("WORKDIR requires exactly one argument")
 	}
 	}
@@ -167,7 +167,7 @@ func workdir(b *Builder, args []string, attributes map[string]bool) error {
 // RUN echo hi          # sh -c echo hi
 // RUN echo hi          # sh -c echo hi
 // RUN [ "echo", "hi" ] # echo hi
 // RUN [ "echo", "hi" ] # echo hi
 //
 //
-func run(b *Builder, args []string, attributes map[string]bool) error {
+func run(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if b.image == "" {
 	if b.image == "" {
 		return fmt.Errorf("Please provide a source image with `from` prior to run")
 		return fmt.Errorf("Please provide a source image with `from` prior to run")
 	}
 	}
@@ -230,7 +230,7 @@ func run(b *Builder, args []string, attributes map[string]bool) error {
 // Set the default command to run in the container (which may be empty).
 // Set the default command to run in the container (which may be empty).
 // Argument handling is the same as RUN.
 // Argument handling is the same as RUN.
 //
 //
-func cmd(b *Builder, args []string, attributes map[string]bool) error {
+func cmd(b *Builder, args []string, attributes map[string]bool, original string) error {
 	b.Config.Cmd = handleJsonArgs(args, attributes)
 	b.Config.Cmd = handleJsonArgs(args, attributes)
 
 
 	if !attributes["json"] && len(b.Config.Entrypoint) == 0 {
 	if !attributes["json"] && len(b.Config.Entrypoint) == 0 {
@@ -256,7 +256,7 @@ func cmd(b *Builder, args []string, attributes map[string]bool) error {
 // Handles command processing similar to CMD and RUN, only b.Config.Entrypoint
 // Handles command processing similar to CMD and RUN, only b.Config.Entrypoint
 // is initialized at NewBuilder time instead of through argument parsing.
 // is initialized at NewBuilder time instead of through argument parsing.
 //
 //
-func entrypoint(b *Builder, args []string, attributes map[string]bool) error {
+func entrypoint(b *Builder, args []string, attributes map[string]bool, original string) error {
 	parsed := handleJsonArgs(args, attributes)
 	parsed := handleJsonArgs(args, attributes)
 
 
 	switch {
 	switch {
@@ -289,7 +289,7 @@ func entrypoint(b *Builder, args []string, attributes map[string]bool) error {
 // Expose ports for links and port mappings. This all ends up in
 // Expose ports for links and port mappings. This all ends up in
 // b.Config.ExposedPorts for runconfig.
 // b.Config.ExposedPorts for runconfig.
 //
 //
-func expose(b *Builder, args []string, attributes map[string]bool) error {
+func expose(b *Builder, args []string, attributes map[string]bool, original string) error {
 	portsTab := args
 	portsTab := args
 
 
 	if b.Config.ExposedPorts == nil {
 	if b.Config.ExposedPorts == nil {
@@ -316,7 +316,7 @@ func expose(b *Builder, args []string, attributes map[string]bool) error {
 // Set the user to 'foo' for future commands and when running the
 // Set the user to 'foo' for future commands and when running the
 // ENTRYPOINT/CMD at container run time.
 // ENTRYPOINT/CMD at container run time.
 //
 //
-func user(b *Builder, args []string, attributes map[string]bool) error {
+func user(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) != 1 {
 	if len(args) != 1 {
 		return fmt.Errorf("USER requires exactly one argument")
 		return fmt.Errorf("USER requires exactly one argument")
 	}
 	}
@@ -329,7 +329,7 @@ func user(b *Builder, args []string, attributes map[string]bool) error {
 //
 //
 // Expose the volume /foo for use. Will also accept the JSON array form.
 // Expose the volume /foo for use. Will also accept the JSON array form.
 //
 //
-func volume(b *Builder, args []string, attributes map[string]bool) error {
+func volume(b *Builder, args []string, attributes map[string]bool, original string) error {
 	if len(args) == 0 {
 	if len(args) == 0 {
 		return fmt.Errorf("Volume cannot be empty")
 		return fmt.Errorf("Volume cannot be empty")
 	}
 	}
@@ -347,6 +347,6 @@ func volume(b *Builder, args []string, attributes map[string]bool) error {
 }
 }
 
 
 // INSERT is no longer accepted, but we still parse it.
 // INSERT is no longer accepted, but we still parse it.
-func insert(b *Builder, args []string, attributes map[string]bool) error {
+func insert(b *Builder, args []string, attributes map[string]bool, original string) error {
 	return fmt.Errorf("INSERT has been deprecated. Please use ADD instead")
 	return fmt.Errorf("INSERT has been deprecated. Please use ADD instead")
 }
 }

+ 4 - 3
builder/evaluator.go

@@ -41,10 +41,10 @@ var (
 	ErrDockerfileEmpty = errors.New("Dockerfile cannot be empty")
 	ErrDockerfileEmpty = errors.New("Dockerfile cannot be empty")
 )
 )
 
 
-var evaluateTable map[string]func(*Builder, []string, map[string]bool) error
+var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error
 
 
 func init() {
 func init() {
-	evaluateTable = map[string]func(*Builder, []string, map[string]bool) error{
+	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
 		"env":        env,
 		"env":        env,
 		"maintainer": maintainer,
 		"maintainer": maintainer,
 		"add":        add,
 		"add":        add,
@@ -190,6 +190,7 @@ func (b *Builder) Run(context io.Reader) (string, error) {
 func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
 func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
 	cmd := ast.Value
 	cmd := ast.Value
 	attrs := ast.Attributes
 	attrs := ast.Attributes
+	original := ast.Original
 	strs := []string{}
 	strs := []string{}
 	msg := fmt.Sprintf("Step %d : %s", stepN, strings.ToUpper(cmd))
 	msg := fmt.Sprintf("Step %d : %s", stepN, strings.ToUpper(cmd))
 
 
@@ -210,7 +211,7 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
 	// XXX yes, we skip any cmds that are not valid; the parser should have
 	// XXX yes, we skip any cmds that are not valid; the parser should have
 	// picked these out already.
 	// picked these out already.
 	if f, ok := evaluateTable[cmd]; ok {
 	if f, ok := evaluateTable[cmd]; ok {
-		return f(b, strs, attrs)
+		return f(b, strs, attrs, original)
 	}
 	}
 
 
 	fmt.Fprintf(b.ErrStream, "# Skipping unknown instruction %s\n", strings.ToUpper(cmd))
 	fmt.Fprintf(b.ErrStream, "# Skipping unknown instruction %s\n", strings.ToUpper(cmd))

+ 14 - 17
builder/internals.go

@@ -18,6 +18,7 @@ import (
 	"syscall"
 	"syscall"
 	"time"
 	"time"
 
 
+	"github.com/docker/docker/builder/parser"
 	"github.com/docker/docker/daemon"
 	"github.com/docker/docker/daemon"
 	imagepkg "github.com/docker/docker/image"
 	imagepkg "github.com/docker/docker/image"
 	"github.com/docker/docker/pkg/archive"
 	"github.com/docker/docker/pkg/archive"
@@ -436,30 +437,26 @@ func (b *Builder) processImageFrom(img *imagepkg.Image) error {
 	onBuildTriggers := b.Config.OnBuild
 	onBuildTriggers := b.Config.OnBuild
 	b.Config.OnBuild = []string{}
 	b.Config.OnBuild = []string{}
 
 
-	// FIXME rewrite this so that builder/parser is used; right now steps in
-	// onbuild are muted because we have no good way to represent the step
-	// number
+	// parse the ONBUILD triggers by invoking the parser
 	for stepN, step := range onBuildTriggers {
 	for stepN, step := range onBuildTriggers {
-		splitStep := strings.Split(step, " ")
-		stepInstruction := strings.ToUpper(strings.Trim(splitStep[0], " "))
-		switch stepInstruction {
-		case "ONBUILD":
-			return fmt.Errorf("Source image contains forbidden chained `ONBUILD ONBUILD` trigger: %s", step)
-		case "MAINTAINER", "FROM":
-			return fmt.Errorf("Source image contains forbidden %s trigger: %s", stepInstruction, step)
+		ast, err := parser.Parse(strings.NewReader(step))
+		if err != nil {
+			return err
 		}
 		}
 
 
-		// FIXME we have to run the evaluator manually here. This does not belong
-		// in this function. Once removed, the init() in evaluator.go should no
-		// longer be necessary.
+		for i, n := range ast.Children {
+			switch strings.ToUpper(n.Value) {
+			case "ONBUILD":
+				return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
+			case "MAINTAINER", "FROM":
+				return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value)
+			}
 
 
-		if f, ok := evaluateTable[strings.ToLower(stepInstruction)]; ok {
 			fmt.Fprintf(b.OutStream, "Trigger %d, %s\n", stepN, step)
 			fmt.Fprintf(b.OutStream, "Trigger %d, %s\n", stepN, step)
-			if err := f(b, splitStep[1:], nil); err != nil {
+
+			if err := b.dispatch(i, n); err != nil {
 				return err
 				return err
 			}
 			}
-		} else {
-			return fmt.Errorf("%s doesn't appear to be a valid Dockerfile instruction", splitStep[0])
 		}
 		}
 	}
 	}
 
 

+ 2 - 0
builder/parser/parser.go

@@ -26,6 +26,7 @@ type Node struct {
 	Next       *Node           // the next item in the current sexp
 	Next       *Node           // the next item in the current sexp
 	Children   []*Node         // the children of this sexp
 	Children   []*Node         // the children of this sexp
 	Attributes map[string]bool // special attributes for this node
 	Attributes map[string]bool // special attributes for this node
+	Original   string          // original line used before parsing
 }
 }
 
 
 var (
 var (
@@ -84,6 +85,7 @@ func parseLine(line string) (string, *Node, error) {
 	if sexp.Value != "" || sexp.Next != nil || sexp.Children != nil {
 	if sexp.Value != "" || sexp.Next != nil || sexp.Children != nil {
 		node.Next = sexp
 		node.Next = sexp
 		node.Attributes = attrs
 		node.Attributes = attrs
+		node.Original = line
 	}
 	}
 
 
 	return "", node, nil
 	return "", node, nil

+ 162 - 2
integration-cli/docker_cli_build_test.go

@@ -7,6 +7,7 @@ import (
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
@@ -14,6 +15,165 @@ import (
 	"github.com/docker/docker/pkg/archive"
 	"github.com/docker/docker/pkg/archive"
 )
 )
 
 
+func TestBuildOnBuildForbiddenMaintainerInSourceImage(t *testing.T) {
+	name := "testbuildonbuildforbiddenmaintainerinsourceimage"
+	defer deleteImages(name)
+	createCmd := exec.Command(dockerBinary, "create", "busybox", "true")
+	out, _, _, err := runCommandWithStdoutStderr(createCmd)
+	errorOut(err, t, out)
+
+	cleanedContainerID := stripTrailingCharacters(out)
+
+	commitCmd := exec.Command(dockerBinary, "commit", "--run", "{\"OnBuild\":[\"MAINTAINER docker.io\"]}", cleanedContainerID, "onbuild")
+
+	if _, err := runCommand(commitCmd); err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = buildImage(name,
+		`FROM onbuild`,
+		true)
+	if err != nil {
+		if !strings.Contains(err.Error(), "maintainer isn't allowed as an ONBUILD trigger") {
+			t.Fatalf("Wrong error %v, must be about MAINTAINER and ONBUILD in source image", err)
+		}
+	} else {
+		t.Fatal("Error must not be nil")
+	}
+	logDone("build - onbuild forbidden maintainer in source image")
+
+}
+
+func TestBuildOnBuildForbiddenFromInSourceImage(t *testing.T) {
+	name := "testbuildonbuildforbiddenfrominsourceimage"
+	defer deleteImages(name)
+	createCmd := exec.Command(dockerBinary, "create", "busybox", "true")
+	out, _, _, err := runCommandWithStdoutStderr(createCmd)
+	errorOut(err, t, out)
+
+	cleanedContainerID := stripTrailingCharacters(out)
+
+	commitCmd := exec.Command(dockerBinary, "commit", "--run", "{\"OnBuild\":[\"FROM busybox\"]}", cleanedContainerID, "onbuild")
+
+	if _, err := runCommand(commitCmd); err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = buildImage(name,
+		`FROM onbuild`,
+		true)
+	if err != nil {
+		if !strings.Contains(err.Error(), "from isn't allowed as an ONBUILD trigger") {
+			t.Fatalf("Wrong error %v, must be about FROM and ONBUILD in source image", err)
+		}
+	} else {
+		t.Fatal("Error must not be nil")
+	}
+	logDone("build - onbuild forbidden from in source image")
+
+}
+
+func TestBuildOnBuildForbiddenChainedInSourceImage(t *testing.T) {
+	name := "testbuildonbuildforbiddenchainedinsourceimage"
+	defer deleteImages(name)
+	createCmd := exec.Command(dockerBinary, "create", "busybox", "true")
+	out, _, _, err := runCommandWithStdoutStderr(createCmd)
+	errorOut(err, t, out)
+
+	cleanedContainerID := stripTrailingCharacters(out)
+
+	commitCmd := exec.Command(dockerBinary, "commit", "--run", "{\"OnBuild\":[\"ONBUILD RUN ls\"]}", cleanedContainerID, "onbuild")
+
+	if _, err := runCommand(commitCmd); err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = buildImage(name,
+		`FROM onbuild`,
+		true)
+	if err != nil {
+		if !strings.Contains(err.Error(), "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") {
+			t.Fatalf("Wrong error %v, must be about chaining ONBUILD in source image", err)
+		}
+	} else {
+		t.Fatal("Error must not be nil")
+	}
+	logDone("build - onbuild forbidden chained in source image")
+
+}
+
+func TestBuildOnBuildCmdEntrypointJSON(t *testing.T) {
+	name1 := "onbuildcmd"
+	name2 := "onbuildgenerated"
+
+	defer deleteAllContainers()
+	defer deleteImages(name2)
+	defer deleteImages(name1)
+
+	_, err := buildImage(name1, `
+FROM busybox
+ONBUILD CMD ["hello world"]
+ONBUILD ENTRYPOINT ["echo"]
+ONBUILD RUN ["true"]`,
+		false)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = buildImage(name2, fmt.Sprintf(`FROM %s`, name1), false)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-t", name2))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !regexp.MustCompile(`(?m)^hello world`).MatchString(out) {
+		t.Fatal("did not get echo output from onbuild", out)
+	}
+
+	logDone("build - onbuild with json entrypoint/cmd")
+}
+
+func TestBuildOnBuildEntrypointJSON(t *testing.T) {
+	name1 := "onbuildcmd"
+	name2 := "onbuildgenerated"
+
+	defer deleteAllContainers()
+	defer deleteImages(name2)
+	defer deleteImages(name1)
+
+	_, err := buildImage(name1, `
+FROM busybox
+ONBUILD ENTRYPOINT ["echo"]`,
+		false)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = buildImage(name2, fmt.Sprintf("FROM %s\nCMD [\"hello world\"]\n", name1), false)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-t", name2))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !regexp.MustCompile(`(?m)^hello world`).MatchString(out) {
+		t.Fatal("got malformed output from onbuild", out)
+	}
+
+	logDone("build - onbuild with json entrypoint")
+}
+
 func TestBuildCacheADD(t *testing.T) {
 func TestBuildCacheADD(t *testing.T) {
 	name := "testbuildtwoimageswithadd"
 	name := "testbuildtwoimageswithadd"
 	defer deleteImages(name)
 	defer deleteImages(name)
@@ -2386,8 +2546,8 @@ func TestBuildOnBuildOutput(t *testing.T) {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 
 
-	if !strings.Contains(out, "Trigger 0, run echo foo") {
-		t.Fatal("failed to find the ONBUILD output")
+	if !strings.Contains(out, "Trigger 0, RUN echo foo") {
+		t.Fatal("failed to find the ONBUILD output", out)
 	}
 	}
 
 
 	logDone("build - onbuild output")
 	logDone("build - onbuild output")