Browse code

Merge pull request #32063 from tonistiigi/named-contexts

Add support for named build stages

Victor Vieux authored on 2017/04/05 04:53:59
Showing 5 changed files
... ...
@@ -187,20 +187,16 @@ func dispatchCopy(b *Builder, args []string, attributes map[string]bool, origina
187 187
 		return err
188 188
 	}
189 189
 
190
-	var contextID *int
190
+	var im *imageMount
191 191
 	if flFrom.IsUsed() {
192 192
 		var err error
193
-		context, err := strconv.Atoi(flFrom.Value)
193
+		im, err = b.imageContexts.get(flFrom.Value)
194 194
 		if err != nil {
195
-			return errors.Wrap(err, "from expects an integer value corresponding to the context number")
196
-		}
197
-		if err := b.imageContexts.validate(context); err != nil {
198 195
 			return err
199 196
 		}
200
-		contextID = &context
201 197
 	}
202 198
 
203
-	return b.runContextCommand(args, false, false, "COPY", contextID)
199
+	return b.runContextCommand(args, false, false, "COPY", im)
204 200
 }
205 201
 
206 202
 // FROM imagename
... ...
@@ -208,7 +204,13 @@ func dispatchCopy(b *Builder, args []string, attributes map[string]bool, origina
208 208
 // This sets the image the dockerfile will build on top of.
209 209
 //
210 210
 func from(b *Builder, args []string, attributes map[string]bool, original string) error {
211
-	if len(args) != 1 {
211
+	ctxName := ""
212
+	if len(args) == 3 && strings.EqualFold(args[1], "as") {
213
+		ctxName = strings.ToLower(args[2])
214
+		if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", ctxName); !ok {
215
+			return errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", ctxName)
216
+		}
217
+	} else if len(args) != 1 {
212 218
 		return errExactlyOneArgument("FROM")
213 219
 	}
214 220
 
... ...
@@ -221,29 +223,32 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
221 221
 	var image builder.Image
222 222
 
223 223
 	b.resetImageCache()
224
-	b.imageContexts.new()
224
+	if _, err := b.imageContexts.new(ctxName, true); err != nil {
225
+		return err
226
+	}
225 227
 
226
-	// Windows cannot support a container with no base image.
227
-	if name == api.NoBaseImageSpecifier {
228
-		if runtime.GOOS == "windows" {
229
-			return errors.New("Windows does not support FROM scratch")
228
+	if im, ok := b.imageContexts.byName[name]; ok {
229
+		if len(im.ImageID()) > 0 {
230
+			image = im
230 231
 		}
231
-		b.image = ""
232
-		b.noBaseImage = true
233 232
 	} else {
234
-		// TODO: don't use `name`, instead resolve it to a digest
235
-		if !b.options.PullParent {
236
-			image, _ = b.docker.GetImageOnBuild(name)
237
-			// TODO: shouldn't we error out if error is different from "not found" ?
238
-		}
239
-		if image == nil {
233
+		// Windows cannot support a container with no base image.
234
+		if name == api.NoBaseImageSpecifier {
235
+			if runtime.GOOS == "windows" {
236
+				return errors.New("Windows does not support FROM scratch")
237
+			}
238
+			b.image = ""
239
+			b.noBaseImage = true
240
+		} else {
240 241
 			var err error
241
-			image, err = b.docker.PullOnBuild(b.clientCtx, name, b.options.AuthConfigs, b.Output)
242
+			image, err = pullOrGetImage(b, name)
242 243
 			if err != nil {
243 244
 				return err
244 245
 			}
245 246
 		}
246
-		b.imageContexts.update(image.ImageID())
247
+	}
248
+	if image != nil {
249
+		b.imageContexts.update(image.ImageID(), image.RunConfig())
247 250
 	}
248 251
 	b.from = image
249 252
 
... ...
@@ -831,3 +836,33 @@ func getShell(c *container.Config) []string {
831 831
 	}
832 832
 	return append([]string{}, c.Shell[:]...)
833 833
 }
834
+
835
+// mountByRef creates an imageMount from a reference. pulling the image if needed.
836
+func mountByRef(b *Builder, name string) (*imageMount, error) {
837
+	image, err := pullOrGetImage(b, name)
838
+	if err != nil {
839
+		return nil, err
840
+	}
841
+	im, err := b.imageContexts.new("", false)
842
+	if err != nil {
843
+		return nil, err
844
+	}
845
+	im.id = image.ImageID()
846
+	return im, nil
847
+}
848
+
849
+func pullOrGetImage(b *Builder, name string) (builder.Image, error) {
850
+	var image builder.Image
851
+	if !b.options.PullParent {
852
+		image, _ = b.docker.GetImageOnBuild(name)
853
+		// TODO: shouldn't we error out if error is different from "not found" ?
854
+	}
855
+	if image == nil {
856
+		var err error
857
+		image, err = b.docker.PullOnBuild(b.clientCtx, name, b.options.AuthConfigs, b.Output)
858
+		if err != nil {
859
+			return nil, err
860
+		}
861
+	}
862
+	return image, nil
863
+}
... ...
@@ -1,9 +1,12 @@
1 1
 package dockerfile
2 2
 
3 3
 import (
4
+	"strconv"
5
+	"strings"
4 6
 	"sync"
5 7
 
6 8
 	"github.com/Sirupsen/logrus"
9
+	"github.com/docker/docker/api/types/container"
7 10
 	"github.com/docker/docker/builder"
8 11
 	"github.com/docker/docker/builder/remotecontext"
9 12
 	"github.com/pkg/errors"
... ...
@@ -12,23 +15,32 @@ import (
12 12
 // imageContexts is a helper for stacking up built image rootfs and reusing
13 13
 // them as contexts
14 14
 type imageContexts struct {
15
-	b     *Builder
16
-	list  []*imageMount
17
-	cache *pathCache
15
+	b      *Builder
16
+	list   []*imageMount
17
+	byName map[string]*imageMount
18
+	cache  *pathCache
18 19
 }
19 20
 
20
-type imageMount struct {
21
-	id      string
22
-	ctx     builder.Context
23
-	release func() error
24
-}
25
-
26
-func (ic *imageContexts) new() {
27
-	ic.list = append(ic.list, &imageMount{})
21
+func (ic *imageContexts) new(name string, increment bool) (*imageMount, error) {
22
+	im := &imageMount{ic: ic}
23
+	if len(name) > 0 {
24
+		if ic.byName == nil {
25
+			ic.byName = make(map[string]*imageMount)
26
+		}
27
+		if _, ok := ic.byName[name]; ok {
28
+			return nil, errors.Errorf("duplicate name %s", name)
29
+		}
30
+		ic.byName[name] = im
31
+	}
32
+	if increment {
33
+		ic.list = append(ic.list, im)
34
+	}
35
+	return im, nil
28 36
 }
29 37
 
30
-func (ic *imageContexts) update(imageID string) {
38
+func (ic *imageContexts) update(imageID string, runConfig *container.Config) {
31 39
 	ic.list[len(ic.list)-1].id = imageID
40
+	ic.list[len(ic.list)-1].runConfig = runConfig
32 41
 }
33 42
 
34 43
 func (ic *imageContexts) validate(i int) error {
... ...
@@ -42,16 +54,72 @@ func (ic *imageContexts) validate(i int) error {
42 42
 	return nil
43 43
 }
44 44
 
45
-func (ic *imageContexts) context(i int) (builder.Context, error) {
46
-	if err := ic.validate(i); err != nil {
47
-		return nil, err
45
+func (ic *imageContexts) get(indexOrName string) (*imageMount, error) {
46
+	index, err := strconv.Atoi(indexOrName)
47
+	if err == nil {
48
+		if err := ic.validate(index); err != nil {
49
+			return nil, err
50
+		}
51
+		return ic.list[index], nil
52
+	}
53
+	if im, ok := ic.byName[strings.ToLower(indexOrName)]; ok {
54
+		return im, nil
55
+	}
56
+	im, err := mountByRef(ic.b, indexOrName)
57
+	if err != nil {
58
+		return nil, errors.Wrapf(err, "invalid from flag value %s", indexOrName)
59
+	}
60
+	return im, nil
61
+}
62
+
63
+func (ic *imageContexts) unmount() (retErr error) {
64
+	for _, im := range ic.list {
65
+		if err := im.unmount(); err != nil {
66
+			logrus.Error(err)
67
+			retErr = err
68
+		}
69
+	}
70
+	for _, im := range ic.byName {
71
+		if err := im.unmount(); err != nil {
72
+			logrus.Error(err)
73
+			retErr = err
74
+		}
75
+	}
76
+	return
77
+}
78
+
79
+func (ic *imageContexts) getCache(id, path string) (interface{}, bool) {
80
+	if ic.cache != nil {
81
+		if id == "" {
82
+			return nil, false
83
+		}
84
+		return ic.cache.get(id + path)
85
+	}
86
+	return nil, false
87
+}
88
+
89
+func (ic *imageContexts) setCache(id, path string, v interface{}) {
90
+	if ic.cache != nil {
91
+		ic.cache.set(id+path, v)
48 92
 	}
49
-	im := ic.list[i]
93
+}
94
+
95
+// imageMount is a reference for getting access to a buildcontext that is backed
96
+// by an existing image
97
+type imageMount struct {
98
+	id        string
99
+	ctx       builder.Context
100
+	release   func() error
101
+	ic        *imageContexts
102
+	runConfig *container.Config
103
+}
104
+
105
+func (im *imageMount) context() (builder.Context, error) {
50 106
 	if im.ctx == nil {
51 107
 		if im.id == "" {
52 108
 			return nil, errors.Errorf("could not copy from empty context")
53 109
 		}
54
-		p, release, err := ic.b.docker.MountImage(im.id)
110
+		p, release, err := im.ic.b.docker.MountImage(im.id)
55 111
 		if err != nil {
56 112
 			return nil, errors.Wrapf(err, "failed to mount %s", im.id)
57 113
 		}
... ...
@@ -59,40 +127,27 @@ func (ic *imageContexts) context(i int) (builder.Context, error) {
59 59
 		if err != nil {
60 60
 			return nil, errors.Wrapf(err, "failed to create lazycontext for %s", p)
61 61
 		}
62
-		logrus.Debugf("mounted image: %s %s", im.id, p)
63 62
 		im.release = release
64 63
 		im.ctx = ctx
65 64
 	}
66 65
 	return im.ctx, nil
67 66
 }
68 67
 
69
-func (ic *imageContexts) unmount() (retErr error) {
70
-	for _, im := range ic.list {
71
-		if im.release != nil {
72
-			if err := im.release(); err != nil {
73
-				logrus.Error(errors.Wrapf(err, "failed to unmount previous build image"))
74
-				retErr = err
75
-			}
68
+func (im *imageMount) unmount() error {
69
+	if im.release != nil {
70
+		if err := im.release(); err != nil {
71
+			return errors.Wrapf(err, "failed to unmount previous build image %s", im.id)
76 72
 		}
73
+		im.release = nil
77 74
 	}
78
-	return
75
+	return nil
79 76
 }
80 77
 
81
-func (ic *imageContexts) getCache(i int, path string) (interface{}, bool) {
82
-	if ic.cache != nil {
83
-		im := ic.list[i]
84
-		if im.id == "" {
85
-			return nil, false
86
-		}
87
-		return ic.cache.get(im.id + path)
88
-	}
89
-	return nil, false
78
+func (im *imageMount) ImageID() string {
79
+	return im.id
90 80
 }
91
-
92
-func (ic *imageContexts) setCache(i int, path string, v interface{}) {
93
-	if ic.cache != nil {
94
-		ic.cache.set(ic.list[i].id+path, v)
95
-	}
81
+func (im *imageMount) RunConfig() *container.Config {
82
+	return im.runConfig
96 83
 }
97 84
 
98 85
 type pathCache struct {
... ...
@@ -85,7 +85,7 @@ func (b *Builder) commit(id string, autoCmd strslice.StrSlice, comment string) e
85 85
 	}
86 86
 
87 87
 	b.image = imageID
88
-	b.imageContexts.update(imageID)
88
+	b.imageContexts.update(imageID, &autoConfig)
89 89
 	return nil
90 90
 }
91 91
 
... ...
@@ -94,7 +94,7 @@ type copyInfo struct {
94 94
 	decompress bool
95 95
 }
96 96
 
97
-func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalDecompression bool, cmdName string, contextID *int) error {
97
+func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalDecompression bool, cmdName string, imageSource *imageMount) error {
98 98
 	if len(args) < 2 {
99 99
 		return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName)
100 100
 	}
... ...
@@ -128,7 +128,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
128 128
 			continue
129 129
 		}
130 130
 		// not a URL
131
-		subInfos, err := b.calcCopyInfo(cmdName, orig, allowLocalDecompression, true, contextID)
131
+		subInfos, err := b.calcCopyInfo(cmdName, orig, allowLocalDecompression, true, imageSource)
132 132
 		if err != nil {
133 133
 			return err
134 134
 		}
... ...
@@ -298,13 +298,13 @@ func (b *Builder) download(srcURL string) (fi builder.FileInfo, err error) {
298 298
 	return &builder.HashedFileInfo{FileInfo: builder.PathFileInfo{FileInfo: tmpFileSt, FilePath: tmpFileName}, FileHash: hash}, nil
299 299
 }
300 300
 
301
-func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression, allowWildcards bool, contextID *int) ([]copyInfo, error) {
301
+func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression, allowWildcards bool, imageSource *imageMount) ([]copyInfo, error) {
302 302
 
303 303
 	// Work in daemon-specific OS filepath semantics
304 304
 	origPath = filepath.FromSlash(origPath)
305 305
 
306 306
 	// validate windows paths from other images
307
-	if contextID != nil && runtime.GOOS == "windows" {
307
+	if imageSource != nil && runtime.GOOS == "windows" {
308 308
 		forbid := regexp.MustCompile("(?i)^" + string(os.PathSeparator) + "?(windows(" + string(os.PathSeparator) + ".+)?)?$")
309 309
 		if p := filepath.Clean(origPath); p == "." || forbid.MatchString(p) {
310 310
 			return nil, errors.Errorf("copy from %s is not allowed on windows", origPath)
... ...
@@ -318,8 +318,8 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
318 318
 
319 319
 	context := b.context
320 320
 	var err error
321
-	if contextID != nil {
322
-		context, err = b.imageContexts.context(*contextID)
321
+	if imageSource != nil {
322
+		context, err = imageSource.context()
323 323
 		if err != nil {
324 324
 			return nil, err
325 325
 		}
... ...
@@ -346,7 +346,7 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
346 346
 
347 347
 			// Note we set allowWildcards to false in case the name has
348 348
 			// a * in it
349
-			subInfos, err := b.calcCopyInfo(cmdName, path, allowLocalDecompression, false, contextID)
349
+			subInfos, err := b.calcCopyInfo(cmdName, path, allowLocalDecompression, false, imageSource)
350 350
 			if err != nil {
351 351
 				return err
352 352
 			}
... ...
@@ -370,9 +370,9 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
370 370
 	if !handleHash {
371 371
 		return copyInfos, nil
372 372
 	}
373
-	if contextID != nil {
373
+	if imageSource != nil {
374 374
 		// fast-cache based on imageID
375
-		if h, ok := b.imageContexts.getCache(*contextID, origPath); ok {
375
+		if h, ok := b.imageContexts.getCache(imageSource.id, origPath); ok {
376 376
 			hfi.SetHash(h.(string))
377 377
 			return copyInfos, nil
378 378
 		}
... ...
@@ -401,8 +401,8 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
401 401
 	hasher := sha256.New()
402 402
 	hasher.Write([]byte(strings.Join(subfiles, ",")))
403 403
 	hfi.SetHash("dir:" + hex.EncodeToString(hasher.Sum(nil)))
404
-	if contextID != nil {
405
-		b.imageContexts.setCache(*contextID, origPath, hfi.Hash())
404
+	if imageSource != nil {
405
+		b.imageContexts.setCache(imageSource.id, origPath, hfi.Hash())
406 406
 	}
407 407
 
408 408
 	return copyInfos, nil
... ...
@@ -497,7 +497,7 @@ func (b *Builder) probeCache() (bool, error) {
497 497
 	fmt.Fprint(b.Stdout, " ---> Using cache\n")
498 498
 	logrus.Debugf("[BUILDER] Use cached version: %s", b.runConfig.Cmd)
499 499
 	b.image = string(cache)
500
-	b.imageContexts.update(b.image)
500
+	b.imageContexts.update(b.image, b.runConfig)
501 501
 
502 502
 	return true, nil
503 503
 }
... ...
@@ -106,7 +106,7 @@ func init() {
106 106
 		command.Entrypoint:  parseMaybeJSON,
107 107
 		command.Env:         parseEnv,
108 108
 		command.Expose:      parseStringsWhitespaceDelimited,
109
-		command.From:        parseString,
109
+		command.From:        parseStringsWhitespaceDelimited,
110 110
 		command.Healthcheck: parseHealthConfig,
111 111
 		command.Label:       parseLabel,
112 112
 		command.Maintainer:  parseString,
... ...
@@ -5752,7 +5752,7 @@ func (s *DockerSuite) TestBuildContChar(c *check.C) {
5752 5752
 
5753 5753
 func (s *DockerSuite) TestBuildCopyFromPreviousRootFS(c *check.C) {
5754 5754
 	dockerfile := `
5755
-		FROM busybox
5755
+		FROM busybox AS first
5756 5756
 		COPY foo bar
5757 5757
 		FROM busybox
5758 5758
     %s
... ...
@@ -5762,7 +5762,8 @@ func (s *DockerSuite) TestBuildCopyFromPreviousRootFS(c *check.C) {
5762 5762
     COPY bar /
5763 5763
     COPY --from=1 baz sub/
5764 5764
     COPY --from=0 bar baz
5765
-    COPY --from=0 bar bay`
5765
+    COPY --from=first bar bay`
5766
+
5766 5767
 	ctx := fakeContext(c, fmt.Sprintf(dockerfile, ""), map[string]string{
5767 5768
 		"Dockerfile": dockerfile,
5768 5769
 		"foo":        "abc",
... ...
@@ -5830,7 +5831,7 @@ func (s *DockerSuite) TestBuildCopyFromPreviousRootFSErrors(c *check.C) {
5830 5830
 
5831 5831
 	buildImage("build1", withExternalBuildContext(ctx)).Assert(c, icmd.Expected{
5832 5832
 		ExitCode: 1,
5833
-		Err:      "from expects an integer value corresponding to the context number",
5833
+		Err:      "invalid from flag value foo",
5834 5834
 	})
5835 5835
 
5836 5836
 	dockerfile = `
... ...
@@ -5847,6 +5848,36 @@ func (s *DockerSuite) TestBuildCopyFromPreviousRootFSErrors(c *check.C) {
5847 5847
 		ExitCode: 1,
5848 5848
 		Err:      "invalid from flag value 0 refers current build block",
5849 5849
 	})
5850
+
5851
+	dockerfile = `
5852
+		FROM busybox AS foo
5853
+		COPY --from=bar foo bar`
5854
+
5855
+	ctx = fakeContext(c, dockerfile, map[string]string{
5856
+		"Dockerfile": dockerfile,
5857
+		"foo":        "abc",
5858
+	})
5859
+	defer ctx.Close()
5860
+
5861
+	buildImage("build1", withExternalBuildContext(ctx)).Assert(c, icmd.Expected{
5862
+		ExitCode: 1,
5863
+		Err:      "invalid from flag value bar",
5864
+	})
5865
+
5866
+	dockerfile = `
5867
+		FROM busybox AS 1
5868
+		COPY --from=1 foo bar`
5869
+
5870
+	ctx = fakeContext(c, dockerfile, map[string]string{
5871
+		"Dockerfile": dockerfile,
5872
+		"foo":        "abc",
5873
+	})
5874
+	defer ctx.Close()
5875
+
5876
+	buildImage("build1", withExternalBuildContext(ctx)).Assert(c, icmd.Expected{
5877
+		ExitCode: 1,
5878
+		Err:      "invalid name for build stage",
5879
+	})
5850 5880
 }
5851 5881
 
5852 5882
 func (s *DockerSuite) TestBuildCopyFromPreviousFrom(c *check.C) {
... ...
@@ -5863,9 +5894,9 @@ func (s *DockerSuite) TestBuildCopyFromPreviousFrom(c *check.C) {
5863 5863
 	result.Assert(c, icmd.Success)
5864 5864
 
5865 5865
 	dockerfile = `
5866
-		FROM build1:latest
5866
+		FROM build1:latest AS foo
5867 5867
     FROM busybox
5868
-		COPY --from=0 bar /
5868
+		COPY --from=foo bar /
5869 5869
 		COPY foo /`
5870 5870
 	ctx = fakeContext(c, dockerfile, map[string]string{
5871 5871
 		"Dockerfile": dockerfile,
... ...
@@ -5882,6 +5913,117 @@ func (s *DockerSuite) TestBuildCopyFromPreviousFrom(c *check.C) {
5882 5882
 	c.Assert(strings.TrimSpace(out), check.Equals, "def")
5883 5883
 }
5884 5884
 
5885
+func (s *DockerSuite) TestBuildCopyFromImplicitFrom(c *check.C) {
5886
+	dockerfile := `
5887
+		FROM busybox
5888
+		COPY --from=busybox /etc/passwd /mypasswd
5889
+		RUN cmp /etc/passwd /mypasswd`
5890
+
5891
+	if DaemonIsWindows() {
5892
+		dockerfile = `
5893
+			FROM busybox
5894
+			COPY --from=busybox License.txt foo`
5895
+	}
5896
+
5897
+	ctx := fakeContext(c, dockerfile, map[string]string{
5898
+		"Dockerfile": dockerfile,
5899
+	})
5900
+	defer ctx.Close()
5901
+
5902
+	result := buildImage("build1", withExternalBuildContext(ctx))
5903
+	result.Assert(c, icmd.Success)
5904
+
5905
+	if DaemonIsWindows() {
5906
+		out, _ := dockerCmd(c, "run", "build1", "cat", "License.txt")
5907
+		c.Assert(len(out), checker.GreaterThan, 10)
5908
+		out2, _ := dockerCmd(c, "run", "build1", "cat", "foo")
5909
+		c.Assert(out, check.Equals, out2)
5910
+	}
5911
+}
5912
+
5913
+func (s *DockerRegistrySuite) TestBuildCopyFromImplicitPullingFrom(c *check.C) {
5914
+	repoName := fmt.Sprintf("%v/dockercli/testf", privateRegistryURL)
5915
+
5916
+	dockerfile := `
5917
+		FROM busybox
5918
+		COPY foo bar`
5919
+	ctx := fakeContext(c, dockerfile, map[string]string{
5920
+		"Dockerfile": dockerfile,
5921
+		"foo":        "abc",
5922
+	})
5923
+	defer ctx.Close()
5924
+
5925
+	result := buildImage(repoName, withExternalBuildContext(ctx))
5926
+	result.Assert(c, icmd.Success)
5927
+
5928
+	dockerCmd(c, "push", repoName)
5929
+	dockerCmd(c, "rmi", repoName)
5930
+
5931
+	dockerfile = `
5932
+		FROM busybox
5933
+		COPY --from=%s bar baz`
5934
+
5935
+	ctx = fakeContext(c, fmt.Sprintf(dockerfile, repoName), map[string]string{
5936
+		"Dockerfile": dockerfile,
5937
+	})
5938
+	defer ctx.Close()
5939
+
5940
+	result = buildImage("build1", withExternalBuildContext(ctx))
5941
+	result.Assert(c, icmd.Success)
5942
+
5943
+	dockerCmdWithResult("run", "build1", "cat", "baz").Assert(c, icmd.Expected{Out: "abc"})
5944
+}
5945
+
5946
+func (s *DockerSuite) TestBuildFromPreviousBlock(c *check.C) {
5947
+	dockerfile := `
5948
+		FROM busybox as foo
5949
+		COPY foo /
5950
+		FROM foo as foo1
5951
+		RUN echo 1 >> foo
5952
+		FROM foo as foO2
5953
+		RUN echo 2 >> foo
5954
+		FROM foo
5955
+		COPY --from=foo1 foo f1
5956
+		COPY --from=FOo2 foo f2
5957
+		` // foo2 case also tests that names are canse insensitive
5958
+	ctx := fakeContext(c, dockerfile, map[string]string{
5959
+		"Dockerfile": dockerfile,
5960
+		"foo":        "bar",
5961
+	})
5962
+	defer ctx.Close()
5963
+
5964
+	result := buildImage("build1", withExternalBuildContext(ctx))
5965
+	result.Assert(c, icmd.Success)
5966
+
5967
+	dockerCmdWithResult("run", "build1", "cat", "foo").Assert(c, icmd.Expected{Out: "bar"})
5968
+
5969
+	dockerCmdWithResult("run", "build1", "cat", "f1").Assert(c, icmd.Expected{Out: "bar1"})
5970
+
5971
+	dockerCmdWithResult("run", "build1", "cat", "f2").Assert(c, icmd.Expected{Out: "bar2"})
5972
+}
5973
+
5974
+func (s *DockerTrustSuite) TestCopyFromTrustedBuild(c *check.C) {
5975
+	img1 := s.setupTrustedImage(c, "trusted-build1")
5976
+	img2 := s.setupTrustedImage(c, "trusted-build2")
5977
+	dockerFile := fmt.Sprintf(`
5978
+	FROM %s AS build-base
5979
+	RUN echo ok > /foo
5980
+	FROM %s
5981
+	COPY --from=build-base foo bar`, img1, img2)
5982
+
5983
+	name := "testcopyfromtrustedbuild"
5984
+
5985
+	r := buildImage(name, trustedBuild, build.WithDockerfile(dockerFile))
5986
+	r.Assert(c, icmd.Expected{
5987
+		Out: fmt.Sprintf("FROM %s@sha", img1[:len(img1)-7]),
5988
+	})
5989
+	r.Assert(c, icmd.Expected{
5990
+		Out: fmt.Sprintf("FROM %s@sha", img2[:len(img2)-7]),
5991
+	})
5992
+
5993
+	dockerCmdWithResult("run", name, "cat", "bar").Assert(c, icmd.Expected{Out: "ok"})
5994
+}
5995
+
5885 5996
 // TestBuildOpaqueDirectory tests that a build succeeds which
5886 5997
 // creates opaque directories.
5887 5998
 // See https://github.com/docker/docker/issues/25244