Selaa lähdekoodia

Allow for Dockerfile to be named something else.
Add a check to make sure Dockerfile is in the build context
Add docs and a testcase
Make -f relative to current dir, not build context

Signed-off-by: Doug Davis <dug@us.ibm.com>

Doug Davis 11 vuotta sitten
vanhempi
commit
eb3ea3b43c

+ 46 - 6
api/client/commands.go

@@ -14,6 +14,7 @@ import (
 	"os"
 	"os/exec"
 	"path"
+	"path/filepath"
 	"runtime"
 	"strconv"
 	"strings"
@@ -84,6 +85,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build")
 	forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers, even after unsuccessful builds")
 	pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image")
+	dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile(Default is 'Dockerfile' at context root)")
+
 	cmd.Require(flag.Exact, 1)
 
 	utils.ParseFlags(cmd, args, true)
@@ -109,7 +112,10 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 			if err != nil {
 				return fmt.Errorf("failed to read Dockerfile from STDIN: %v", err)
 			}
-			context, err = archive.Generate("Dockerfile", string(dockerfile))
+			if *dockerfileName == "" {
+				*dockerfileName = api.DefaultDockerfileName
+			}
+			context, err = archive.Generate(*dockerfileName, string(dockerfile))
 		} else {
 			context = ioutil.NopCloser(buf)
 		}
@@ -136,9 +142,40 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 		if _, err := os.Stat(root); err != nil {
 			return err
 		}
-		filename := path.Join(root, "Dockerfile")
+
+		absRoot, err := filepath.Abs(root)
+		if err != nil {
+			return err
+		}
+
+		var filename string       // path to Dockerfile
+		var origDockerfile string // used for error msg
+
+		if *dockerfileName == "" {
+			// No -f/--file was specified so use the default
+			origDockerfile = api.DefaultDockerfileName
+			*dockerfileName = origDockerfile
+			filename = path.Join(absRoot, *dockerfileName)
+		} else {
+			origDockerfile = *dockerfileName
+			if filename, err = filepath.Abs(*dockerfileName); err != nil {
+				return err
+			}
+
+			// Verify that 'filename' is within the build context
+			if !strings.HasSuffix(absRoot, string(os.PathSeparator)) {
+				absRoot += string(os.PathSeparator)
+			}
+			if !strings.HasPrefix(filename, absRoot) {
+				return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", *dockerfileName, root)
+			}
+
+			// Now reset the dockerfileName to be relative to the build context
+			*dockerfileName = filename[len(absRoot):]
+		}
+
 		if _, err = os.Stat(filename); os.IsNotExist(err) {
-			return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0))
+			return fmt.Errorf("Can not locate Dockerfile: %s", origDockerfile)
 		}
 		var includes []string = []string{"."}
 
@@ -147,16 +184,16 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 			return err
 		}
 
-		// If .dockerignore mentions .dockerignore or Dockerfile
+		// If .dockerignore mentions .dockerignore or the Dockerfile
 		// then make sure we send both files over to the daemon
 		// because Dockerfile is, obviously, needed no matter what, and
 		// .dockerignore is needed to know if either one needs to be
 		// removed.  The deamon will remove them for us, if needed, after it
 		// parses the Dockerfile.
 		keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
-		keepThem2, _ := fileutils.Matches("Dockerfile", excludes)
+		keepThem2, _ := fileutils.Matches(*dockerfileName, excludes)
 		if keepThem1 || keepThem2 {
-			includes = append(includes, ".dockerignore", "Dockerfile")
+			includes = append(includes, ".dockerignore", *dockerfileName)
 		}
 
 		if err = utils.ValidateContextDirectory(root, excludes); err != nil {
@@ -219,6 +256,9 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	if *pull {
 		v.Set("pull", "1")
 	}
+
+	v.Set("dockerfile", *dockerfileName)
+
 	cli.LoadConfigFile()
 
 	headers := http.Header(make(map[string][]string))

+ 4 - 3
api/common.go

@@ -15,9 +15,10 @@ import (
 )
 
 const (
-	APIVERSION        version.Version = "1.16"
-	DEFAULTHTTPHOST                   = "127.0.0.1"
-	DEFAULTUNIXSOCKET                 = "/var/run/docker.sock"
+	APIVERSION            version.Version = "1.16"
+	DEFAULTHTTPHOST                       = "127.0.0.1"
+	DEFAULTUNIXSOCKET                     = "/var/run/docker.sock"
+	DefaultDockerfileName string          = "Dockerfile"
 )
 
 func ValidateHost(val string) (string, error) {

+ 1 - 0
api/server/server.go

@@ -1035,6 +1035,7 @@ func postBuild(eng *engine.Engine, version version.Version, w http.ResponseWrite
 	}
 	job.Stdin.Add(r.Body)
 	job.Setenv("remote", r.FormValue("remote"))
+	job.Setenv("dockerfile", r.FormValue("dockerfile"))
 	job.Setenv("t", r.FormValue("t"))
 	job.Setenv("q", r.FormValue("q"))
 	job.Setenv("nocache", r.FormValue("nocache"))

+ 12 - 11
builder/evaluator.go

@@ -105,13 +105,14 @@ type Builder struct {
 	// both of these are controlled by the Remove and ForceRemove options in BuildOpts
 	TmpContainers map[string]struct{} // a map of containers used for removes
 
-	dockerfile  *parser.Node  // the syntax tree of the dockerfile
-	image       string        // image name for commit processing
-	maintainer  string        // maintainer name. could probably be removed.
-	cmdSet      bool          // indicates is CMD was set in current Dockerfile
-	context     tarsum.TarSum // the context is a tarball that is uploaded by the client
-	contextPath string        // the path of the temporary directory the local context is unpacked to (server side)
-	noBaseImage bool          // indicates that this build does not start from any base image, but is being built from an empty file system.
+	dockerfileName string        // name of Dockerfile
+	dockerfile     *parser.Node  // the syntax tree of the dockerfile
+	image          string        // image name for commit processing
+	maintainer     string        // maintainer name. could probably be removed.
+	cmdSet         bool          // indicates is CMD was set in current Dockerfile
+	context        tarsum.TarSum // the context is a tarball that is uploaded by the client
+	contextPath    string        // the path of the temporary directory the local context is unpacked to (server side)
+	noBaseImage    bool          // indicates that this build does not start from any base image, but is being built from an empty file system.
 }
 
 // Run the builder with the context. This is the lynchpin of this package. This
@@ -137,7 +138,7 @@ func (b *Builder) Run(context io.Reader) (string, error) {
 		}
 	}()
 
-	if err := b.readDockerfile("Dockerfile"); err != nil {
+	if err := b.readDockerfile(b.dockerfileName); err != nil {
 		return "", err
 	}
 
@@ -205,9 +206,9 @@ func (b *Builder) readDockerfile(filename string) error {
 		os.Remove(path.Join(b.contextPath, ".dockerignore"))
 		b.context.(tarsum.BuilderContext).Remove(".dockerignore")
 	}
-	if rm, _ := fileutils.Matches("Dockerfile", excludes); rm == true {
-		os.Remove(path.Join(b.contextPath, "Dockerfile"))
-		b.context.(tarsum.BuilderContext).Remove("Dockerfile")
+	if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true {
+		os.Remove(path.Join(b.contextPath, b.dockerfileName))
+		b.context.(tarsum.BuilderContext).Remove(b.dockerfileName)
 	}
 
 	return nil

+ 9 - 1
builder/job.go

@@ -6,6 +6,7 @@ import (
 	"os"
 	"os/exec"
 
+	"github.com/docker/docker/api"
 	"github.com/docker/docker/daemon"
 	"github.com/docker/docker/engine"
 	"github.com/docker/docker/graph"
@@ -30,6 +31,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
 		return job.Errorf("Usage: %s\n", job.Name)
 	}
 	var (
+		dockerfileName = job.Getenv("dockerfile")
 		remoteURL      = job.Getenv("remote")
 		repoName       = job.Getenv("t")
 		suppressOutput = job.GetenvBool("q")
@@ -42,6 +44,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
 		tag            string
 		context        io.ReadCloser
 	)
+
 	job.GetenvJson("authConfig", authConfig)
 	job.GetenvJson("configFile", configFile)
 
@@ -57,6 +60,10 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
 		}
 	}
 
+	if dockerfileName == "" {
+		dockerfileName = api.DefaultDockerfileName
+	}
+
 	if remoteURL == "" {
 		context = ioutil.NopCloser(job.Stdin)
 	} else if urlutil.IsGitURL(remoteURL) {
@@ -88,7 +95,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
 		if err != nil {
 			return job.Error(err)
 		}
-		c, err := archive.Generate("Dockerfile", string(dockerFile))
+		c, err := archive.Generate(dockerfileName, string(dockerFile))
 		if err != nil {
 			return job.Error(err)
 		}
@@ -118,6 +125,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
 		StreamFormatter: sf,
 		AuthConfig:      authConfig,
 		AuthConfigFile:  configFile,
+		dockerfileName:  dockerfileName,
 	}
 
 	id, err := builder.Run(context)

+ 4 - 0
docs/man/docker-build.1.md

@@ -7,6 +7,7 @@ docker-build - Build a new image from the source code at PATH
 # SYNOPSIS
 **docker build**
 [**--help**]
+[**-f**|**--file**[=*Dockerfile*]]
 [**--force-rm**[=*false*]]
 [**--no-cache**[=*false*]]
 [**-q**|**--quiet**[=*false*]]
@@ -31,6 +32,9 @@ When a Git repository is set as the **URL**, the repository is used
 as context.
 
 # OPTIONS
+**-f**, **--file**=*Dockerfile*
+   Path to the Dockerfile to use. If the path is a relative path then it must be relative to the current directory. The file must be within the build context. The default is *Dockerfile*.
+
 **--force-rm**=*true*|*false*
    Always remove intermediate containers, even after unsuccessful builds. The default is *false*.
 

+ 11 - 6
docs/sources/reference/api/docker_remote_api_v1.17.md

@@ -1157,16 +1157,21 @@ Build an image from Dockerfile via stdin
         {"stream": "..."}
         {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}}
 
-    The stream must be a tar archive compressed with one of the
-    following algorithms: identity (no compression), gzip, bzip2, xz.
+The input stream must be a tar archive compressed with one of the
+following algorithms: identity (no compression), gzip, bzip2, xz.
 
-    The archive must include a file called `Dockerfile`
-    at its root. It may include any number of other files,
-    which will be accessible in the build context (See the [*ADD build
-    command*](/reference/builder/#dockerbuilder)).
+The archive must include a build instructions file, typically called
+`Dockerfile` at the root of the archive. The `f` parameter may be used
+to specify a different build instructions file by having its value be
+the path to the alternate build instructions file to use.
+
+The archive may include any number of other files,
+which will be accessible in the build context (See the [*ADD build
+command*](/reference/builder/#dockerbuilder)).
 
 Query Parameters:
 
+-   **dockerfile** - path within the build context to the Dockerfile
 -   **t** – repository name (and optionally a tag) to be applied to
         the resulting image in case of success
 -   **q** – suppress verbose build output

+ 35 - 5
docs/sources/reference/commandline/cli.md

@@ -461,11 +461,12 @@ To kill the container, use `docker kill`.
 
     Build a new image from the source code at PATH
 
-      --force-rm=false     Always remove intermediate containers, even after unsuccessful builds
-      --no-cache=false     Do not use cache when building the image
-      -q, --quiet=false    Suppress the verbose output generated by the containers
-      --rm=true            Remove intermediate containers after a successful build
-      -t, --tag=""         Repository name (and optionally a tag) to be applied to the resulting image in case of success
+      -f, --file=""            Location of the Dockerfile to use. Default is 'Dockerfile' at the root of the build context
+      --force-rm=false         Always remove intermediate containers, even after unsuccessful builds
+      --no-cache=false         Do not use cache when building the image
+      -q, --quiet=false        Suppress the verbose output generated by the containers
+      --rm=true                Remove intermediate containers after a successful build
+      -t, --tag=""             Repository name (and optionally a tag) to be applied to the resulting image in case of success
 
 Use this command to build Docker images from a Dockerfile and a
 "context".
@@ -510,6 +511,13 @@ For example, the files `tempa`, `tempb` are ignored from the root directory.
 Currently there is no support for regular expressions. Formats
 like `[^temp*]` are ignored.
 
+By default the `docker build` command will look for a `Dockerfile` at the
+root of the build context. The `-f`, `--file`, option lets you specify
+the path to an alternative file to use instead.  This is useful
+in cases where the same set of files are used for multiple builds. The path
+must be to a file within the build context. If a relative path is specified
+then it must to be relative to the current directory.
+
 
 See also:
 
@@ -612,6 +620,28 @@ repository is used as Dockerfile. Note that you
 can specify an arbitrary Git repository by using the `git://` or `git@`
 schema.
 
+    $ sudo docker build -f Dockerfile.debug .
+
+This will use a file called `Dockerfile.debug` for the build
+instructions instead of `Dockerfile`.
+
+    $ sudo docker build -f dockerfiles/Dockerfile.debug -t myapp_debug .
+    $ sudo docker build -f dockerfiles/Dockerfile.prod  -t myapp_prod .
+
+The above commands will build the current build context (as specified by 
+the `.`) twice, once using a debug version of a `Dockerfile` and once using 
+a production version.
+
+    $ cd /home/me/myapp/some/dir/really/deep
+    $ sudo docker build -f /home/me/myapp/dockerfiles/debug /home/me/myapp
+    $ sudo docker build -f ../../../../dockerfiles/debug /home/me/myapp
+
+These two `docker build` commands do the exact same thing. They both
+use the contents of the `debug` file instead of looking for a `Dockerfile` 
+and will use `/home/me/myapp` as the root of the build context. Note that 
+`debug` is in the directory structure of the build context, regardless of how 
+you refer to it on the command line.
+
 > **Note:** `docker build` will return a `no such file or directory` error
 > if the file or directory does not exist in the uploaded context. This may
 > happen if there is no context, or if you specify a file that is elsewhere

+ 131 - 0
integration-cli/docker_cli_build_test.go

@@ -3155,6 +3155,36 @@ func TestBuildDockerignoringDockerfile(t *testing.T) {
 	logDone("build - test .dockerignore of Dockerfile")
 }
 
+func TestBuildDockerignoringRenamedDockerfile(t *testing.T) {
+	name := "testbuilddockerignoredockerfile"
+	defer deleteImages(name)
+	dockerfile := `
+        FROM busybox
+		ADD . /tmp/
+		RUN ls /tmp/Dockerfile
+		RUN ! ls /tmp/MyDockerfile
+		RUN ls /tmp/.dockerignore`
+	ctx, err := fakeContext(dockerfile, map[string]string{
+		"Dockerfile":    "Should not use me",
+		"MyDockerfile":  dockerfile,
+		".dockerignore": "MyDockerfile\n",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, err = buildImageFromContext(name, ctx, true); err != nil {
+		t.Fatalf("Didn't ignore MyDockerfile correctly:%s", err)
+	}
+
+	// now try it with ./MyDockerfile
+	ctx.Add(".dockerignore", "./MyDockerfile\n")
+	if _, err = buildImageFromContext(name, ctx, true); err != nil {
+		t.Fatalf("Didn't ignore ./MyDockerfile correctly:%s", err)
+	}
+
+	logDone("build - test .dockerignore of renamed Dockerfile")
+}
+
 func TestBuildDockerignoringDockerignore(t *testing.T) {
 	name := "testbuilddockerignoredockerignore"
 	defer deleteImages(name)
@@ -4170,3 +4200,104 @@ CMD cat /foo/file`,
 
 	logDone("build - volumes retain contents in build")
 }
+
+func TestBuildRenamedDockerfile(t *testing.T) {
+	defer deleteAllContainers()
+
+	ctx, err := fakeContext(`FROM busybox
+	RUN echo from Dockerfile`,
+		map[string]string{
+			"Dockerfile":       "FROM busybox\nRUN echo from Dockerfile",
+			"files/Dockerfile": "FROM busybox\nRUN echo from files/Dockerfile",
+			"files/dFile":      "FROM busybox\nRUN echo from files/dFile",
+			"dFile":            "FROM busybox\nRUN echo from dFile",
+		})
+	defer ctx.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	out, _, err := dockerCmdInDir(t, ctx.Dir, "build", "-t", "test1", ".")
+
+	if err != nil {
+		t.Fatalf("Failed to build: %s\n%s", out, err)
+	}
+	if !strings.Contains(out, "from Dockerfile") {
+		t.Fatalf("Should have used Dockerfile, output:%s", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "-f", "files/Dockerfile", "-t", "test2", ".")
+
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !strings.Contains(out, "from files/Dockerfile") {
+		t.Fatalf("Should have used files/Dockerfile, output:%s", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "--file=files/dFile", "-t", "test3", ".")
+
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !strings.Contains(out, "from files/dFile") {
+		t.Fatalf("Should have used files/dFile, output:%s", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "--file=dFile", "-t", "test4", ".")
+
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !strings.Contains(out, "from dFile") {
+		t.Fatalf("Should have used dFile, output:%s", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "--file=/etc/passwd", "-t", "test5", ".")
+
+	if err == nil {
+		t.Fatalf("Was supposed to fail to find passwd")
+	}
+
+	if !strings.Contains(out, "The Dockerfile (/etc/passwd) must be within the build context (.)") {
+		t.Fatalf("Wrong error message for passwd:%v", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir+"/files", "build", "-f", "../Dockerfile", "-t", "test5", "..")
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !strings.Contains(out, "from Dockerfile") {
+		t.Fatalf("Should have used root Dockerfile, output:%s", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir+"/files", "build", "-f", ctx.Dir+"/files/Dockerfile", "-t", "test6", "..")
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !strings.Contains(out, "from files/Dockerfile") {
+		t.Fatalf("Should have used files Dockerfile - 2, output:%s", out)
+	}
+
+	out, _, err = dockerCmdInDir(t, ctx.Dir+"/files", "build", "-f", "../Dockerfile", "-t", "test7", ".")
+
+	if err == nil || !strings.Contains(out, "must be within the build context") {
+		t.Fatalf("Should have failed with Dockerfile out of context")
+	}
+
+	out, _, err = dockerCmdInDir(t, "/tmp", "build", "-t", "test6", ctx.Dir)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !strings.Contains(out, "from Dockerfile") {
+		t.Fatalf("Should have used root Dockerfile, output:%s", out)
+	}
+
+	logDone("build - rename dockerfile")
+}