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>
... | ... |
@@ -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() + ` |