From a1048523d274347179c3b4011c56747f8fbc15ab Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 18 Feb 2018 01:40:55 +0100 Subject: [PATCH] Allow Dockerfile from outside build-context Historically, the Dockerfile had to be insde the build-context, because it was sent as part of the build-context. https://github.com/moby/moby/pull/31236/commits/3f6dc81e10b8b813fffaa9b4167a60c5a507fa38 added support for passing the Dockerfile through stdin, in which case the contents of the Dockerfile is injected into the build-context. This patch uses the same mechanism for situations where the location of the Dockerfile is passed, and its path is outside of the build-context. Before this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context unable to prepare context: the Dockerfile (/Users/sebastiaan/projects/test/dockerfile-outside/myproject/dockerfiles/Dockerfile) must be within the build context After this change: $ mkdir -p myproject/context myproject/dockerfiles && cd myproject $ echo "hello" > context/hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > dockerfiles/Dockerfile $ docker build --no-cache -f $PWD/dockerfiles/Dockerfile $PWD/context Sending build context to Docker daemon 2.607kB Step 1/3 : FROM busybox ---> 6ad733544a63 Step 2/3 : COPY /hello / ---> 9a5ae1c7be9e Step 3/3 : RUN cat /hello ---> Running in 20dfef2d180f hello Removing intermediate container 20dfef2d180f ---> ce1748f91bb2 Successfully built ce1748f91bb2 Signed-off-by: Sebastiaan van Stijn --- cli/command/image/build.go | 14 +++++++-- cli/command/image/build/context.go | 8 ++--- cli/command/image/build_test.go | 50 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 637068a80f4b..dd901bd8ae3b 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -9,8 +9,10 @@ import ( "io" "io/ioutil" "os" + "path/filepath" "regexp" "runtime" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -206,6 +208,14 @@ func runBuild(dockerCli command.Cli, options buildOptions) error { buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) case isLocalDir(specifiedContext): contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) + if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx + dockerfileCtx, err = os.Open(options.dockerfileName) + if err != nil { + return errors.Errorf("unable to open Dockerfile: %v", err) + } + defer dockerfileCtx.Close() + } case urlutil.IsGitURL(specifiedContext): tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) case urlutil.IsURL(specifiedContext): @@ -253,7 +263,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error { } } - // replace Dockerfile if it was added from stdin and there is archive context + // replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context if dockerfileCtx != nil && buildCtx != nil { buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx) if err != nil { @@ -261,7 +271,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error { } } - // if streaming and dockerfile was not from stdin then read from file + // if streaming and Dockerfile was not from stdin then read from file // to the same reader that is usually stdin if options.stream && dockerfileCtx == nil { dockerfileCtx, err = os.Open(relDockerfile) diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index a98cd7b237e1..1d3011aff6bc 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -167,6 +167,10 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) return "", "", err } relDockerfile, err := getDockerfileRelPath(absContextDir, dockerfileName) + if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + return "", "", errors.Errorf("the Dockerfile (%s) must be within the build context", dockerfileName) + } + return absContextDir, relDockerfile, err } @@ -318,10 +322,6 @@ func getDockerfileRelPath(absContextDir, givenDockerfile string) (string, error) return "", errors.Errorf("unable to get relative Dockerfile path: %v", err) } - if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - return "", errors.Errorf("the Dockerfile (%s) must be within the build context", givenDockerfile) - } - return relDockerfile, nil } diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index 1ccd70e45d92..2b672fdc1d46 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -108,6 +108,56 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual) } +func TestRunBuildDockerfileOutsideContext(t *testing.T) { + dir := fs.NewDir(t, t.Name(), + fs.WithFile("data", "data file"), + ) + defer dir.Remove() + + // Dockerfile outside of build-context + df := fs.NewFile(t, t.Name(), + fs.WithContent(` +FROM FOOBAR +COPY data /data + `), + ) + defer df.Remove() + + dest, err := ioutil.TempDir("", t.Name()) + require.NoError(t, err) + defer os.RemoveAll(dest) + + var dockerfileName string + fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + buffer := new(bytes.Buffer) + tee := io.TeeReader(context, buffer) + + assert.NoError(t, archive.Untar(tee, dest, nil)) + dockerfileName = options.Dockerfile + + body := new(bytes.Buffer) + return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil + } + + cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild}) + + options := newBuildOptions() + options.context = dir.Path() + options.dockerfileName = df.Path() + + err = runBuild(cli, options) + require.NoError(t, err) + + files, err := ioutil.ReadDir(dest) + require.NoError(t, err) + var actual []string + for _, fileInfo := range files { + actual = append(actual, fileInfo.Name()) + } + sort.Strings(actual) + assert.Equal(t, []string{dockerfileName, ".dockerignore", "data"}, actual) +} + // TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts // starting with `github.com/` are special-cased, and the build command attempts // to clone the remote repo.