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.