diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go index 1b875b9dc9..c6c34cac66 100644 --- a/builder/dockerfile/dispatchers.go +++ b/builder/dockerfile/dispatchers.go @@ -189,15 +189,20 @@ func dispatchCopy(b *Builder, args []string, attributes map[string]bool, origina var contextID *int if flFrom.IsUsed() { - var err error - context, err := strconv.Atoi(flFrom.Value) - if err != nil { - return errors.Wrap(err, "from expects an integer value corresponding to the context number") + flFrom.Value = strings.ToLower(flFrom.Value) + if context, ok := b.imageContexts.byName[flFrom.Value]; ok { + contextID = &context + } else { + var err error + context, err := strconv.Atoi(flFrom.Value) + if err != nil { + return errors.Wrap(err, "from expects an integer value corresponding to the context number") + } + if err := b.imageContexts.validate(context); err != nil { + return err + } + contextID = &context } - if err := b.imageContexts.validate(context); err != nil { - return err - } - contextID = &context } return b.runContextCommand(args, false, false, "COPY", contextID) @@ -208,7 +213,13 @@ func dispatchCopy(b *Builder, args []string, attributes map[string]bool, origina // This sets the image the dockerfile will build on top of. // func from(b *Builder, args []string, attributes map[string]bool, original string) error { - if len(args) != 1 { + ctxName := "" + if len(args) == 3 && strings.EqualFold(args[1], "as") { + ctxName = strings.ToLower(args[2]) + if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", ctxName); !ok { + return errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", ctxName) + } + } else if len(args) != 1 { return errExactlyOneArgument("FROM") } @@ -221,7 +232,9 @@ func from(b *Builder, args []string, attributes map[string]bool, original string var image builder.Image b.resetImageCache() - b.imageContexts.new() + if err := b.imageContexts.new(ctxName); err != nil { + return err + } // Windows cannot support a container with no base image. if name == api.NoBaseImageSpecifier { diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index 32ce2aacbc..803c96d7d6 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -12,9 +12,10 @@ import ( // imageContexts is a helper for stacking up built image rootfs and reusing // them as contexts type imageContexts struct { - b *Builder - list []*imageMount - cache *pathCache + b *Builder + list []*imageMount + byName map[string]int + cache *pathCache } type imageMount struct { @@ -23,8 +24,18 @@ type imageMount struct { release func() error } -func (ic *imageContexts) new() { +func (ic *imageContexts) new(name string) error { + if len(name) > 0 { + if ic.byName == nil { + ic.byName = make(map[string]int) + } + if _, ok := ic.byName[name]; ok { + return errors.Errorf("duplicate name %s", name) + } + ic.byName[name] = len(ic.list) + } ic.list = append(ic.list, &imageMount{}) + return nil } func (ic *imageContexts) update(imageID string) { diff --git a/builder/dockerfile/parser/parser.go b/builder/dockerfile/parser/parser.go index 1b2dc8f3a0..b0d4d9c2d0 100644 --- a/builder/dockerfile/parser/parser.go +++ b/builder/dockerfile/parser/parser.go @@ -80,7 +80,7 @@ func init() { command.Entrypoint: parseMaybeJSON, command.Env: parseEnv, command.Expose: parseStringsWhitespaceDelimited, - command.From: parseString, + command.From: parseStringsWhitespaceDelimited, command.Healthcheck: parseHealthConfig, command.Label: parseLabel, command.Maintainer: parseString, diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index bd712615ab..95a5b7bc7d 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -5752,7 +5752,7 @@ func (s *DockerSuite) TestBuildContChar(c *check.C) { func (s *DockerSuite) TestBuildCopyFromPreviousRootFS(c *check.C) { dockerfile := ` - FROM busybox + FROM busybox AS first COPY foo bar FROM busybox %s @@ -5762,7 +5762,8 @@ func (s *DockerSuite) TestBuildCopyFromPreviousRootFS(c *check.C) { COPY bar / COPY --from=1 baz sub/ COPY --from=0 bar baz - COPY --from=0 bar bay` + COPY --from=first bar bay` + ctx := fakeContext(c, fmt.Sprintf(dockerfile, ""), map[string]string{ "Dockerfile": dockerfile, "foo": "abc", @@ -5847,6 +5848,36 @@ func (s *DockerSuite) TestBuildCopyFromPreviousRootFSErrors(c *check.C) { ExitCode: 1, Err: "invalid from flag value 0 refers current build block", }) + + dockerfile = ` + FROM busybox AS foo + COPY --from=bar foo bar` + + ctx = fakeContext(c, dockerfile, map[string]string{ + "Dockerfile": dockerfile, + "foo": "abc", + }) + defer ctx.Close() + + buildImage("build1", withExternalBuildContext(ctx)).Assert(c, icmd.Expected{ + ExitCode: 1, + Err: "invalid context value bar", + }) + + dockerfile = ` + FROM busybox AS 1 + COPY --from=1 foo bar` + + ctx = fakeContext(c, dockerfile, map[string]string{ + "Dockerfile": dockerfile, + "foo": "abc", + }) + defer ctx.Close() + + buildImage("build1", withExternalBuildContext(ctx)).Assert(c, icmd.Expected{ + ExitCode: 1, + Err: "invalid name for build stage", + }) } func (s *DockerSuite) TestBuildCopyFromPreviousFrom(c *check.C) { @@ -5863,9 +5894,9 @@ func (s *DockerSuite) TestBuildCopyFromPreviousFrom(c *check.C) { result.Assert(c, icmd.Success) dockerfile = ` - FROM build1:latest + FROM build1:latest AS foo FROM busybox - COPY --from=0 bar / + COPY --from=foo bar / COPY foo /` ctx = fakeContext(c, dockerfile, map[string]string{ "Dockerfile": dockerfile,