diff --git a/api/server/router/build/build_routes.go b/api/server/router/build/build_routes.go index 6d4835dd10..ba86d80fbf 100644 --- a/api/server/router/build/build_routes.go +++ b/api/server/router/build/build_routes.go @@ -56,6 +56,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui options.ExtraHosts = r.Form["extrahosts"] options.SecurityOpt = r.Form["securityopt"] options.Squash = httputils.BoolValue(r, "squash") + options.Target = r.FormValue("target") if r.Form.Get("shmsize") != "" { shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) diff --git a/api/types/client.go b/api/types/client.go index 56ec211293..2bf6ad01a9 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -176,6 +176,7 @@ type ImageBuildOptions struct { CacheFrom []string SecurityOpt []string ExtraHosts []string // List of extra hosts + Target string } // ImageBuildResponse holds information diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index c4f7d20440..3d2eb104da 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder" + "github.com/docker/docker/builder/dockerfile/command" "github.com/docker/docker/builder/dockerfile/parser" "github.com/docker/docker/image" "github.com/docker/docker/pkg/stringid" @@ -253,6 +254,10 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri // Not cancelled yet, keep going... } + if command.From == n.Value && b.imageContexts.isCurrentTarget(b.options.Target) { + break + } + if err := b.dispatch(i, total, n); err != nil { if b.options.ForceRemove { b.clearTmp() @@ -267,6 +272,10 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri } } + if b.options.Target != "" && !b.imageContexts.isCurrentTarget(b.options.Target) { + return "", perrors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target) + } + b.warnOnUnusedBuildArgs() if b.image == "" { diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index 60fa1d2d0d..1b92ced179 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -15,10 +15,11 @@ import ( // imageContexts is a helper for stacking up built image rootfs and reusing // them as contexts type imageContexts struct { - b *Builder - list []*imageMount - byName map[string]*imageMount - cache *pathCache + b *Builder + list []*imageMount + byName map[string]*imageMount + cache *pathCache + currentName string } func (ic *imageContexts) new(name string, increment bool) (*imageMount, error) { @@ -35,6 +36,7 @@ func (ic *imageContexts) new(name string, increment bool) (*imageMount, error) { if increment { ic.list = append(ic.list, im) } + ic.currentName = name return im, nil } @@ -88,6 +90,13 @@ func (ic *imageContexts) unmount() (retErr error) { return } +func (ic *imageContexts) isCurrentTarget(target string) bool { + if target == "" { + return false + } + return strings.EqualFold(ic.currentName, target) +} + func (ic *imageContexts) getCache(id, path string) (interface{}, bool) { if ic.cache != nil { if id == "" { diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 5268cbc254..27fe83c524 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -64,6 +64,7 @@ type buildOptions struct { securityOpt []string networkMode string squash bool + target string } // NewBuildCommand creates a new `docker build` command @@ -115,6 +116,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") flags.SetAnnotation("network", "version", []string{"1.25"}) flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") + flags.StringVar(&options.target, "target", "", "Set the target build stage to build.") command.AddTrustVerificationFlags(flags) @@ -302,6 +304,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { NetworkMode: options.networkMode, Squash: options.squash, ExtraHosts: options.extraHosts.GetAll(), + Target: options.target, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) diff --git a/client/image_build.go b/client/image_build.go index cc5a71c2a1..bb69143e99 100644 --- a/client/image_build.go +++ b/client/image_build.go @@ -95,6 +95,7 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur query.Set("cgroupparent", options.CgroupParent) query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10)) query.Set("dockerfile", options.Dockerfile) + query.Set("target", options.Target) ulimitsJSON, err := json.Marshal(options.Ulimits) if err != nil { diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 4bb0a4cec7..ea25959e44 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -6210,6 +6210,33 @@ func (s *DockerSuite) TestBuildCopyFromWindowsIsCaseInsensitive(c *check.C) { result.Assert(c, exp) } +func (s *DockerSuite) TestBuildIntermediateTarget(c *check.C) { + dockerfile := ` + FROM busybox AS build-env + CMD ["/dev"] + FROM busybox + CMD ["/dist"] + ` + ctx := fakeContext(c, dockerfile, map[string]string{ + "Dockerfile": dockerfile, + }) + defer ctx.Close() + + result := buildImage("build1", withExternalBuildContext(ctx), + cli.WithFlags("--target", "build-env")) + result.Assert(c, icmd.Success) + + res := inspectFieldJSON(c, "build1", "Config.Cmd") + c.Assert(res, checker.Equals, `["/dev"]`) + + result = buildImage("build1", withExternalBuildContext(ctx), + cli.WithFlags("--target", "nosuchtarget")) + result.Assert(c, icmd.Expected{ + ExitCode: 1, + Err: "failed to reach build target", + }) +} + // TestBuildOpaqueDirectory tests that a build succeeds which // creates opaque directories. // See https://github.com/docker/docker/issues/25244