Browse code

Rebase --chown function for ADD/COPY

Rebases and completes initial PR for (prior: --user) --chown flag for
ADD/COPY commands in Dockerfile.

Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com>

Phil Estes authored on 2017/07/27 01:05:55
Showing 9 changed files
... ...
@@ -56,6 +56,7 @@ type copyInstruction struct {
56 56
 	cmdName                 string
57 57
 	infos                   []copyInfo
58 58
 	dest                    string
59
+	chownStr                string
59 60
 	allowLocalDecompression bool
60 61
 }
61 62
 
... ...
@@ -369,6 +370,7 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b
369 369
 type copyFileOptions struct {
370 370
 	decompress bool
371 371
 	archiver   *archive.Archiver
372
+	chownPair  idtools.IDPair
372 373
 }
373 374
 
374 375
 func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions) error {
... ...
@@ -388,7 +390,7 @@ func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions)
388 388
 		return errors.Wrapf(err, "source path not found")
389 389
 	}
390 390
 	if src.IsDir() {
391
-		return copyDirectory(archiver, srcPath, destPath)
391
+		return copyDirectory(archiver, srcPath, destPath, options.chownPair)
392 392
 	}
393 393
 	if options.decompress && archive.IsArchivePath(srcPath) && !source.noDecompress {
394 394
 		return archiver.UntarPath(srcPath, destPath)
... ...
@@ -405,26 +407,28 @@ func performCopyForInfo(dest copyInfo, source copyInfo, options copyFileOptions)
405 405
 		// is a symlink
406 406
 		destPath = filepath.Join(destPath, filepath.Base(source.path))
407 407
 	}
408
-	return copyFile(archiver, srcPath, destPath)
408
+	return copyFile(archiver, srcPath, destPath, options.chownPair)
409 409
 }
410 410
 
411
-func copyDirectory(archiver *archive.Archiver, source, dest string) error {
411
+func copyDirectory(archiver *archive.Archiver, source, dest string, chownPair idtools.IDPair) error {
412
+	destExists, err := isExistingDirectory(dest)
413
+	if err != nil {
414
+		return errors.Wrapf(err, "failed to query destination path")
415
+	}
412 416
 	if err := archiver.CopyWithTar(source, dest); err != nil {
413 417
 		return errors.Wrapf(err, "failed to copy directory")
414 418
 	}
415
-	return fixPermissions(source, dest, archiver.IDMappings.RootPair())
419
+	return fixPermissions(source, dest, chownPair, !destExists)
416 420
 }
417 421
 
418
-func copyFile(archiver *archive.Archiver, source, dest string) error {
419
-	rootIDs := archiver.IDMappings.RootPair()
420
-
421
-	if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest), 0755, rootIDs); err != nil {
422
+func copyFile(archiver *archive.Archiver, source, dest string, chownPair idtools.IDPair) error {
423
+	if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest), 0755, chownPair); err != nil {
422 424
 		return errors.Wrapf(err, "failed to create new directory")
423 425
 	}
424 426
 	if err := archiver.CopyFileWithTar(source, dest); err != nil {
425 427
 		return errors.Wrapf(err, "failed to copy file")
426 428
 	}
427
-	return fixPermissions(source, dest, rootIDs)
429
+	return fixPermissions(source, dest, chownPair, false)
428 430
 }
429 431
 
430 432
 func endsInSlash(path string) bool {
... ...
@@ -9,10 +9,16 @@ import (
9 9
 	"github.com/docker/docker/pkg/idtools"
10 10
 )
11 11
 
12
-func fixPermissions(source, destination string, rootIDs idtools.IDPair) error {
13
-	skipChownRoot, err := isExistingDirectory(destination)
14
-	if err != nil {
15
-		return err
12
+func fixPermissions(source, destination string, rootIDs idtools.IDPair, overrideSkip bool) error {
13
+	var (
14
+		skipChownRoot bool
15
+		err           error
16
+	)
17
+	if !overrideSkip {
18
+		skipChownRoot, err = isExistingDirectory(destination)
19
+		if err != nil {
20
+			return err
21
+		}
16 22
 	}
17 23
 
18 24
 	// We Walk on the source rather than on the destination because we don't
... ...
@@ -2,7 +2,7 @@ package dockerfile
2 2
 
3 3
 import "github.com/docker/docker/pkg/idtools"
4 4
 
5
-func fixPermissions(source, destination string, rootIDs idtools.IDPair) error {
5
+func fixPermissions(source, destination string, rootIDs idtools.IDPair, overrideSkip bool) error {
6 6
 	// chown is not supported on Windows
7 7
 	return nil
8 8
 }
... ...
@@ -158,6 +158,7 @@ func add(req dispatchRequest) error {
158 158
 	if err != nil {
159 159
 		return err
160 160
 	}
161
+	copyInstruction.chownStr = flChown.Value
161 162
 	copyInstruction.allowLocalDecompression = true
162 163
 
163 164
 	return req.builder.performCopy(req.state, copyInstruction)
... ...
@@ -189,6 +190,7 @@ func dispatchCopy(req dispatchRequest) error {
189 189
 	if err != nil {
190 190
 		return err
191 191
 	}
192
+	copyInstruction.chownStr = flChown.Value
192 193
 
193 194
 	return req.builder.performCopy(req.state, copyInstruction)
194 195
 }
... ...
@@ -7,13 +7,18 @@ import (
7 7
 	"crypto/sha256"
8 8
 	"encoding/hex"
9 9
 	"fmt"
10
+	"path/filepath"
11
+	"strconv"
10 12
 	"strings"
11 13
 
12 14
 	"github.com/docker/docker/api/types"
13 15
 	"github.com/docker/docker/api/types/backend"
14 16
 	"github.com/docker/docker/api/types/container"
15 17
 	"github.com/docker/docker/image"
18
+	"github.com/docker/docker/pkg/idtools"
16 19
 	"github.com/docker/docker/pkg/stringid"
20
+	"github.com/docker/docker/pkg/symlink"
21
+	lcUser "github.com/opencontainers/runc/libcontainer/user"
17 22
 	"github.com/pkg/errors"
18 23
 )
19 24
 
... ...
@@ -107,10 +112,16 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC
107 107
 func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error {
108 108
 	srcHash := getSourceHashFromInfos(inst.infos)
109 109
 
110
+	var chownComment string
111
+	if inst.chownStr != "" {
112
+		chownComment = fmt.Sprintf("--chown=%s", inst.chownStr)
113
+	}
114
+	commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest)
115
+
110 116
 	// TODO: should this have been using origPaths instead of srcHash in the comment?
111 117
 	runConfigWithCommentCmd := copyRunConfig(
112 118
 		state.runConfig,
113
-		withCmdCommentString(fmt.Sprintf("%s %s in %s ", inst.cmdName, srcHash, inst.dest), b.platform))
119
+		withCmdCommentString(commentStr, b.platform))
114 120
 	hit, err := b.probeCache(state, runConfigWithCommentCmd)
115 121
 	if err != nil || hit {
116 122
 		return err
... ...
@@ -125,9 +136,21 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error
125 125
 		return err
126 126
 	}
127 127
 
128
+	chownPair := b.archiver.IDMappings.RootPair()
129
+	// if a chown was requested, perform the steps to get the uid, gid
130
+	// translated (if necessary because of user namespaces), and replace
131
+	// the root pair with the chown pair for copy operations
132
+	if inst.chownStr != "" {
133
+		chownPair, err = parseChownFlag(inst.chownStr, destInfo.root, b.archiver.IDMappings)
134
+		if err != nil {
135
+			return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping")
136
+		}
137
+	}
138
+
128 139
 	opts := copyFileOptions{
129 140
 		decompress: inst.allowLocalDecompression,
130 141
 		archiver:   b.archiver,
142
+		chownPair:  chownPair,
131 143
 	}
132 144
 	for _, info := range inst.infos {
133 145
 		if err := performCopyForInfo(destInfo, info, opts); err != nil {
... ...
@@ -137,6 +160,88 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error
137 137
 	return b.exportImage(state, imageMount, runConfigWithCommentCmd)
138 138
 }
139 139
 
140
+func parseChownFlag(chown, ctrRootPath string, idMappings *idtools.IDMappings) (idtools.IDPair, error) {
141
+	var userStr, grpStr string
142
+	parts := strings.Split(chown, ":")
143
+	if len(parts) > 2 {
144
+		return idtools.IDPair{}, errors.New("invalid chown string format: " + chown)
145
+	}
146
+	if len(parts) == 1 {
147
+		// if no group specified, use the user spec as group as well
148
+		userStr, grpStr = parts[0], parts[0]
149
+	} else {
150
+		userStr, grpStr = parts[0], parts[1]
151
+	}
152
+
153
+	passwdPath, err := symlink.FollowSymlinkInScope(filepath.Join(ctrRootPath, "etc", "passwd"), ctrRootPath)
154
+	if err != nil {
155
+		return idtools.IDPair{}, errors.Wrapf(err, "can't resolve /etc/passwd path in container rootfs")
156
+	}
157
+	groupPath, err := symlink.FollowSymlinkInScope(filepath.Join(ctrRootPath, "etc", "group"), ctrRootPath)
158
+	if err != nil {
159
+		return idtools.IDPair{}, errors.Wrapf(err, "can't resolve /etc/group path in container rootfs")
160
+	}
161
+	uid, err := lookupUser(userStr, passwdPath)
162
+	if err != nil {
163
+		return idtools.IDPair{}, errors.Wrapf(err, "can't find uid for user "+userStr)
164
+	}
165
+	gid, err := lookupGroup(grpStr, groupPath)
166
+	if err != nil {
167
+		return idtools.IDPair{}, errors.Wrapf(err, "can't find gid for group "+grpStr)
168
+	}
169
+
170
+	// convert as necessary because of user namespaces
171
+	chownPair, err := idMappings.ToHost(idtools.IDPair{UID: uid, GID: gid})
172
+	if err != nil {
173
+		return idtools.IDPair{}, errors.Wrapf(err, "unable to convert uid/gid to host mapping")
174
+	}
175
+	return chownPair, nil
176
+}
177
+
178
+func lookupUser(userStr, filepath string) (int, error) {
179
+	// if the string is actually a uid integer, parse to int and return
180
+	// as we don't need to translate with the help of files
181
+	uid, err := strconv.Atoi(userStr)
182
+	if err == nil {
183
+		return uid, nil
184
+	}
185
+	users, err := lcUser.ParsePasswdFileFilter(filepath, func(u lcUser.User) bool {
186
+		if u.Name == userStr {
187
+			return true
188
+		}
189
+		return false
190
+	})
191
+	if err != nil {
192
+		return 0, err
193
+	}
194
+	if len(users) == 0 {
195
+		return 0, errors.New("no such user: " + userStr)
196
+	}
197
+	return users[0].Uid, nil
198
+}
199
+
200
+func lookupGroup(groupStr, filepath string) (int, error) {
201
+	// if the string is actually a gid integer, parse to int and return
202
+	// as we don't need to translate with the help of files
203
+	gid, err := strconv.Atoi(groupStr)
204
+	if err == nil {
205
+		return gid, nil
206
+	}
207
+	groups, err := lcUser.ParseGroupFileFilter(filepath, func(g lcUser.Group) bool {
208
+		if g.Name == groupStr {
209
+			return true
210
+		}
211
+		return false
212
+	})
213
+	if err != nil {
214
+		return 0, err
215
+	}
216
+	if len(groups) == 0 {
217
+		return 0, errors.New("no such group: " + groupStr)
218
+	}
219
+	return groups[0].Gid, nil
220
+}
221
+
140 222
 func createDestInfo(workingDir string, inst copyInstruction, imageMount *imageMount) (copyInfo, error) {
141 223
 	// Twiddle the destination when it's a relative path - meaning, make it
142 224
 	// relative to the WORKINGDIR
... ...
@@ -2,6 +2,8 @@ package dockerfile
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"os"
6
+	"path/filepath"
5 7
 	"runtime"
6 8
 	"testing"
7 9
 
... ...
@@ -11,6 +13,7 @@ import (
11 11
 	"github.com/docker/docker/builder"
12 12
 	"github.com/docker/docker/builder/remotecontext"
13 13
 	"github.com/docker/docker/pkg/archive"
14
+	"github.com/docker/docker/pkg/idtools"
14 15
 	"github.com/stretchr/testify/assert"
15 16
 	"github.com/stretchr/testify/require"
16 17
 )
... ...
@@ -129,3 +132,130 @@ func TestCopyRunConfig(t *testing.T) {
129 129
 	}
130 130
 
131 131
 }
132
+
133
+func TestChownFlagParsing(t *testing.T) {
134
+	testFiles := map[string]string{
135
+		"passwd": `root:x:0:0::/bin:/bin/false
136
+bin:x:1:1::/bin:/bin/false
137
+wwwwww:x:21:33::/bin:/bin/false
138
+unicorn:x:1001:1002::/bin:/bin/false
139
+		`,
140
+		"group": `root:x:0:
141
+bin:x:1:
142
+wwwwww:x:33:
143
+unicorn:x:1002:
144
+somegrp:x:5555:
145
+othergrp:x:6666:
146
+		`,
147
+	}
148
+	// test mappings for validating use of maps
149
+	idMaps := []idtools.IDMap{
150
+		{
151
+			ContainerID: 0,
152
+			HostID:      100000,
153
+			Size:        65536,
154
+		},
155
+	}
156
+	remapped := idtools.NewIDMappingsFromMaps(idMaps, idMaps)
157
+	unmapped := &idtools.IDMappings{}
158
+
159
+	contextDir, cleanup := createTestTempDir(t, "", "builder-chown-parse-test")
160
+	defer cleanup()
161
+
162
+	if err := os.Mkdir(filepath.Join(contextDir, "etc"), 0755); err != nil {
163
+		t.Fatalf("error creating test directory: %v", err)
164
+	}
165
+
166
+	for filename, content := range testFiles {
167
+		createTestTempFile(t, filepath.Join(contextDir, "etc"), filename, content, 0644)
168
+	}
169
+
170
+	// positive tests
171
+	for _, testcase := range []struct {
172
+		name      string
173
+		chownStr  string
174
+		idMapping *idtools.IDMappings
175
+		expected  idtools.IDPair
176
+	}{
177
+		{
178
+			name:      "UIDNoMap",
179
+			chownStr:  "1",
180
+			idMapping: unmapped,
181
+			expected:  idtools.IDPair{UID: 1, GID: 1},
182
+		},
183
+		{
184
+			name:      "UIDGIDNoMap",
185
+			chownStr:  "0:1",
186
+			idMapping: unmapped,
187
+			expected:  idtools.IDPair{UID: 0, GID: 1},
188
+		},
189
+		{
190
+			name:      "UIDWithMap",
191
+			chownStr:  "0",
192
+			idMapping: remapped,
193
+			expected:  idtools.IDPair{UID: 100000, GID: 100000},
194
+		},
195
+		{
196
+			name:      "UIDGIDWithMap",
197
+			chownStr:  "1:33",
198
+			idMapping: remapped,
199
+			expected:  idtools.IDPair{UID: 100001, GID: 100033},
200
+		},
201
+		{
202
+			name:      "UserNoMap",
203
+			chownStr:  "bin:5555",
204
+			idMapping: unmapped,
205
+			expected:  idtools.IDPair{UID: 1, GID: 5555},
206
+		},
207
+		{
208
+			name:      "GroupWithMap",
209
+			chownStr:  "0:unicorn",
210
+			idMapping: remapped,
211
+			expected:  idtools.IDPair{UID: 100000, GID: 101002},
212
+		},
213
+		{
214
+			name:      "UserOnlyWithMap",
215
+			chownStr:  "unicorn",
216
+			idMapping: remapped,
217
+			expected:  idtools.IDPair{UID: 101001, GID: 101002},
218
+		},
219
+	} {
220
+		t.Run(testcase.name, func(t *testing.T) {
221
+			idPair, err := parseChownFlag(testcase.chownStr, contextDir, testcase.idMapping)
222
+			require.NoError(t, err, "Failed to parse chown flag: %q", testcase.chownStr)
223
+			assert.Equal(t, testcase.expected, idPair, "chown flag mapping failure")
224
+		})
225
+	}
226
+
227
+	// error tests
228
+	for _, testcase := range []struct {
229
+		name      string
230
+		chownStr  string
231
+		idMapping *idtools.IDMappings
232
+		descr     string
233
+	}{
234
+		{
235
+			name:      "BadChownFlagFormat",
236
+			chownStr:  "bob:1:555",
237
+			idMapping: unmapped,
238
+			descr:     "invalid chown string format: bob:1:555",
239
+		},
240
+		{
241
+			name:      "UserNoExist",
242
+			chownStr:  "bob",
243
+			idMapping: unmapped,
244
+			descr:     "can't find uid for user bob: no such user: bob",
245
+		},
246
+		{
247
+			name:      "GroupNoExist",
248
+			chownStr:  "root:bob",
249
+			idMapping: unmapped,
250
+			descr:     "can't find gid for group bob: no such group: bob",
251
+		},
252
+	} {
253
+		t.Run(testcase.name, func(t *testing.T) {
254
+			_, err := parseChownFlag(testcase.chownStr, contextDir, testcase.idMapping)
255
+			assert.EqualError(t, err, testcase.descr, "Expected error string doesn't match")
256
+		})
257
+	}
258
+}
... ...
@@ -44,7 +44,6 @@ import (
44 44
 	"github.com/docker/libnetwork/options"
45 45
 	"github.com/docker/libnetwork/types"
46 46
 	agentexec "github.com/docker/swarmkit/agent/exec"
47
-	"github.com/opencontainers/runc/libcontainer/user"
48 47
 	"golang.org/x/net/context"
49 48
 )
50 49
 
... ...
@@ -332,36 +331,6 @@ func (container *Container) GetRootResourcePath(path string) (string, error) {
332 332
 	return symlink.FollowSymlinkInScope(filepath.Join(container.Root, cleanPath), container.Root)
333 333
 }
334 334
 
335
-// ParseUserGrp takes `username` in the format of username, uid, username:groupname,
336
-// uid:gid, username:gid, or uid:groupname and parses the passwd file in the container
337
-// to return the ExecUser referred to by `username`.
338
-func (container *Container) ParseUserGrp(username string) (*user.ExecUser, error) {
339
-	passwdPath, err := user.GetPasswdPath()
340
-	if err != nil {
341
-		return nil, err
342
-	}
343
-	passwdPath, err = container.GetResourcePath(passwdPath)
344
-	if err != nil {
345
-		return nil, err
346
-	}
347
-
348
-	groupPath, err := user.GetGroupPath()
349
-	if err != nil {
350
-		return nil, err
351
-	}
352
-	groupPath, err = container.GetResourcePath(groupPath)
353
-	if err != nil {
354
-		return nil, err
355
-	}
356
-
357
-	execUser, err := user.GetExecUserPath(username, nil, passwdPath, groupPath)
358
-	if err != nil {
359
-		return nil, err
360
-	}
361
-
362
-	return execUser, nil
363
-}
364
-
365 335
 // ExitOnNext signals to the monitor that it should not restart the container
366 336
 // after we send the kill signal.
367 337
 func (container *Container) ExitOnNext() {
... ...
@@ -410,6 +410,35 @@ func (s *DockerSuite) TestBuildAddRemoteNoDecompress(c *check.C) {
410 410
 	assert.Contains(c, string(out), "Successfully built")
411 411
 }
412 412
 
413
+func (s *DockerSuite) TestBuildChownOnCopy(c *check.C) {
414
+	testRequires(c, DaemonIsLinux)
415
+	dockerfile := `FROM busybox
416
+		RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd
417
+		RUN echo 'test1:x:1001:' >> /etc/group
418
+		RUN echo 'test2:x:1002:' >> /etc/group
419
+		COPY --chown=test1:1002 . /new_dir
420
+		RUN ls -l /
421
+		RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ]
422
+		RUN [ $(ls -nl / | grep new_dir | awk '{print $3":"$4}') = '1001:1002' ]
423
+	`
424
+	ctx := fakecontext.New(c, "",
425
+		fakecontext.WithDockerfile(dockerfile),
426
+		fakecontext.WithFile("test_file1", "some test content"),
427
+	)
428
+	defer ctx.Close()
429
+
430
+	res, body, err := request.Post(
431
+		"/build",
432
+		request.RawContent(ctx.AsTarReader(c)),
433
+		request.ContentType("application/x-tar"))
434
+	c.Assert(err, checker.IsNil)
435
+	c.Assert(res.StatusCode, checker.Equals, http.StatusOK)
436
+
437
+	out, err := testutil.ReadBody(body)
438
+	require.NoError(c, err)
439
+	assert.Contains(c, string(out), "Successfully built")
440
+}
441
+
413 442
 func (s *DockerSuite) TestBuildWithSession(c *check.C) {
414 443
 	testRequires(c, ExperimentalDaemon)
415 444
 
... ...
@@ -602,78 +602,6 @@ RUN [ $(cat "/test dir/test_file6") = 'test6' ]`, command, command, command, com
602 602
 	}
603 603
 }
604 604
 
605
-func (s *DockerSuite) TestBuildAddChownFlag(c *check.C) {
606
-	testRequires(c, DaemonIsLinux) // Linux specific test
607
-	name := "testaddtonewdest"
608
-	ctx, err := fakeContext(`FROM busybox
609
-RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd
610
-RUN echo 'test1:x:1001:' >> /etc/group
611
-RUN echo 'test2:x:1002:' >> /etc/group
612
-ADD --chown=test1:1002 . /new_dir
613
-RUN ls -l /
614
-RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ]`,
615
-		map[string]string{
616
-			"test_dir/test_file": "test file",
617
-		})
618
-	if err != nil {
619
-		c.Fatal(err)
620
-	}
621
-	defer ctx.Close()
622
-
623
-	if _, err := buildImageFromContext(name, ctx, true); err != nil {
624
-		c.Fatal(err)
625
-	}
626
-}
627
-
628
-func (s *DockerDaemonSuite) TestBuildAddChownFlagUserNamespace(c *check.C) {
629
-	testRequires(c, DaemonIsLinux) // Linux specific test
630
-
631
-	c.Assert(s.d.StartWithBusybox("--userns-remap", "default"), checker.IsNil)
632
-
633
-	name := "testaddtonewdest"
634
-	ctx, err := fakeContext(`FROM busybox
635
-RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd
636
-RUN echo 'test1:x:1001:' >> /etc/group
637
-RUN echo 'test2:x:1002:' >> /etc/group
638
-ADD --chown=test1:1002 . /new_dir
639
-RUN ls -l /
640
-RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ]`,
641
-		map[string]string{
642
-			"test_dir/test_file": "test file",
643
-		})
644
-	if err != nil {
645
-		c.Fatal(err)
646
-	}
647
-	defer ctx.Close()
648
-
649
-	if _, err := buildImageFromContext(name, ctx, true); err != nil {
650
-		c.Fatal(err)
651
-	}
652
-}
653
-
654
-func (s *DockerSuite) TestBuildCopyChownFlag(c *check.C) {
655
-	testRequires(c, DaemonIsLinux) // Linux specific test
656
-	name := "testaddtonewdest"
657
-	ctx, err := fakeContext(`FROM busybox
658
-RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd
659
-RUN echo 'test1:x:1001:' >> /etc/group
660
-RUN echo 'test2:x:1002:' >> /etc/group
661
-COPY --chown=test1:1002 . /new_dir
662
-RUN ls -l /
663
-RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ]`,
664
-		map[string]string{
665
-			"test_dir/test_file": "test file",
666
-		})
667
-	if err != nil {
668
-		c.Fatal(err)
669
-	}
670
-	defer ctx.Close()
671
-
672
-	if _, err := buildImageFromContext(name, ctx, true); err != nil {
673
-		c.Fatal(err)
674
-	}
675
-}
676
-
677 605
 func (s *DockerSuite) TestBuildCopyFileWithWhitespaceOnWindows(c *check.C) {
678 606
 	testRequires(c, DaemonIsWindows)
679 607
 	dockerfile := `FROM ` + testEnv.MinimalBaseImage() + `