From eb3ea3b43c716ad727521a7d0bc20d7321bb0867 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Thu, 11 Sep 2014 07:42:17 -0700 Subject: [PATCH] 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 --- api/client/commands.go | 52 ++++++- api/common.go | 7 +- api/server/server.go | 1 + builder/evaluator.go | 23 +-- builder/job.go | 10 +- docs/man/docker-build.1.md | 4 + .../reference/api/docker_remote_api_v1.17.md | 17 ++- docs/sources/reference/commandline/cli.md | 40 +++++- integration-cli/docker_cli_build_test.go | 131 ++++++++++++++++++ 9 files changed, 253 insertions(+), 32 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 3b22c722ca..7701e17da6 100644 --- a/api/client/commands.go +++ b/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)) diff --git a/api/common.go b/api/common.go index 71e72f69e0..b8e7c84b37 100644 --- a/api/common.go +++ b/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) { diff --git a/api/server/server.go b/api/server/server.go index 6b15962b27..cfaa5f43ab 100644 --- a/api/server/server.go +++ b/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")) diff --git a/builder/evaluator.go b/builder/evaluator.go index 43fb419fce..3149bd0df7 100644 --- a/builder/evaluator.go +++ b/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 diff --git a/builder/job.go b/builder/job.go index 20299d490a..905a8cc998 100644 --- a/builder/job.go +++ b/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) diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index c5dfa706cb..56e0807dff 100644 --- a/docs/man/docker-build.1.md +++ b/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*. diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index e0ed084788..1e4acd7aaf 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/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 diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 2d7caedc7b..799c0148c7 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/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 diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index c4d7ff7c54..a27aecc56f 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/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") +}