Skip to content

Commit da2d4c7

Browse files
committed
Add --parents option for COPY in Dockerfiles
It also includes an implementation of the --parents flag for the buildah copy command. Fixes: https://issues.redhat.com/browse/RUN-2193 Fixes: #5557 Signed-off-by: Jan Rodák <[email protected]>
1 parent 7776f50 commit da2d4c7

File tree

24 files changed

+560
-36
lines changed

24 files changed

+560
-36
lines changed

add.go

+41-2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type AddAndCopyOptions struct {
9494
// RetryDelay is how long to wait before retrying attempts to retrieve
9595
// remote contents.
9696
RetryDelay time.Duration
97+
// Parents preserve parent directories of source content
98+
Parents bool
9799
}
98100

99101
// gitURLFragmentSuffix matches fragments to use as Git reference and build
@@ -263,6 +265,25 @@ func globbedToGlobbable(glob string) string {
263265
return result
264266
}
265267

268+
// getParentsPrefixToRemoveAndParentsToSkip gets from the pattern the prefix before the "pivot point",
269+
// the location in the source path marked by the path component named "."
270+
// (i.e. where "/./" occurs in the path). And list of parents to skip.
271+
// In case "/./" is not present is returned "/".
272+
func getParentsPrefixToRemoveAndParentsToSkip(pattern string, contextDir string) (string, []string) {
273+
prefix, _, found := strings.Cut(strings.TrimPrefix(pattern, contextDir), "/./")
274+
if !found {
275+
return string(filepath.Separator), []string{}
276+
}
277+
prefix = strings.TrimPrefix(filepath.Clean(string(filepath.Separator)+prefix), string(filepath.Separator))
278+
out := []string{}
279+
parentPath := prefix
280+
for parentPath != "/" && parentPath != "." {
281+
out = append(out, parentPath)
282+
parentPath = filepath.Dir(parentPath)
283+
}
284+
return prefix, out
285+
}
286+
266287
// Add copies the contents of the specified sources into the container's root
267288
// filesystem, optionally extracting contents of local files that look like
268289
// non-empty archives.
@@ -476,7 +497,6 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
476497
if err := copier.Mkdir(mountPoint, extractDirectory, mkdirOptions); err != nil {
477498
return fmt.Errorf("ensuring target directory exists: %w", err)
478499
}
479-
480500
// Copy each source in turn.
481501
for _, src := range sources {
482502
var multiErr *multierror.Error
@@ -587,7 +607,6 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
587607
if localSourceStat == nil {
588608
continue
589609
}
590-
591610
// Iterate through every item that matched the glob.
592611
itemsCopied := 0
593612
for _, globbed := range localSourceStat.Globbed {
@@ -640,6 +659,25 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
640659
return false, false, nil
641660
})
642661
}
662+
663+
if options.Parents {
664+
parentsPrefixToRemove, parentsToSkip := getParentsPrefixToRemoveAndParentsToSkip(src, options.ContextDir)
665+
writer = newTarFilterer(writer, func(hdr *tar.Header) (bool, bool, io.Reader) {
666+
skip := false
667+
for _, parent := range parentsToSkip {
668+
if hdr.Name == parent && hdr.Typeflag == tar.TypeDir {
669+
skip = true
670+
break
671+
}
672+
}
673+
hdr.Name = strings.TrimPrefix(hdr.Name, parentsPrefixToRemove)
674+
hdr.Name = strings.TrimPrefix(hdr.Name, "/")
675+
if hdr.Name == "" {
676+
skip = true
677+
}
678+
return skip, false, nil
679+
})
680+
}
643681
writer = newTarFilterer(writer, func(_ *tar.Header) (bool, bool, io.Reader) {
644682
itemsCopied++
645683
return false, false, nil
@@ -656,6 +694,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
656694
StripSetuidBit: options.StripSetuidBit,
657695
StripSetgidBit: options.StripSetgidBit,
658696
StripStickyBit: options.StripStickyBit,
697+
Parents: options.Parents,
659698
}
660699
getErr = copier.Get(contextDir, contextDir, getOptions, []string{globbedToGlobbable(globbed)}, writer)
661700
closeErr = writer.Close()

cmd/buildah/addcopy.go

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type addCopyResults struct {
3838
retry int
3939
retryDelay string
4040
excludes []string
41+
parents bool
4142
}
4243

4344
func createCommand(addCopy string, desc string, short string, opts *addCopyResults) *cobra.Command {
@@ -116,6 +117,7 @@ func init() {
116117

117118
copyFlags := copyCommand.Flags()
118119
applyFlagVars(copyFlags, &copyOpts)
120+
copyFlags.BoolVar(&copyOpts.parents, "parents", false, "preserve leading directories in the paths of items being copied")
119121

120122
rootCmd.AddCommand(addCommand)
121123
rootCmd.AddCommand(copyCommand)
@@ -246,6 +248,7 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe
246248
CertPath: systemContext.DockerCertPath,
247249
InsecureSkipTLSVerify: systemContext.DockerInsecureSkipTLSVerify,
248250
MaxRetries: iopts.retry,
251+
Parents: iopts.parents,
249252
}
250253
if iopts.contextdir != "" {
251254
var excludes []string

copier/copier.go

+119-29
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"os/user"
1414
"path"
1515
"path/filepath"
16+
"slices"
17+
"sort"
1618
"strconv"
1719
"strings"
1820
"sync"
@@ -350,6 +352,7 @@ type GetOptions struct {
350352
ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted
351353
ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted
352354
ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted
355+
Parents bool // maintain the sources parent directory in the destination
353356
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
354357
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
355358
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
@@ -1182,6 +1185,51 @@ func errorIsPermission(err error) bool {
11821185
return errors.Is(err, os.ErrPermission) || strings.Contains(err.Error(), "permission denied")
11831186
}
11841187

1188+
func getParents(path string, stopPath string) []string {
1189+
out := []string{}
1190+
for path != "/" && path != "." && path != stopPath {
1191+
path = filepath.Dir(path)
1192+
if path == stopPath {
1193+
continue
1194+
}
1195+
out = append(out, path)
1196+
}
1197+
sort.Slice(out, func(i, j int) bool {
1198+
return len(out[i]) < len(out[j])
1199+
})
1200+
return out
1201+
}
1202+
1203+
func checkLinks(item string, req request, info os.FileInfo) (string, os.FileInfo, error) {
1204+
// chase links. if we hit a dead end, we should just fail
1205+
oldItem := item
1206+
followedLinks := 0
1207+
const maxFollowedLinks = 16
1208+
for !req.GetOptions.NoDerefSymlinks && info.Mode()&os.ModeType == os.ModeSymlink && followedLinks < maxFollowedLinks {
1209+
path, err := os.Readlink(item)
1210+
if err != nil {
1211+
continue
1212+
}
1213+
if filepath.IsAbs(path) || looksLikeAbs(path) {
1214+
path = filepath.Join(req.Root, path)
1215+
} else {
1216+
path = filepath.Join(filepath.Dir(item), path)
1217+
}
1218+
item = path
1219+
if _, err = convertToRelSubdirectory(req.Root, item); err != nil {
1220+
return "", nil, fmt.Errorf("copier: get: computing path of %q(%q) relative to %q: %w", oldItem, item, req.Root, err)
1221+
}
1222+
if info, err = os.Lstat(item); err != nil {
1223+
return "", nil, fmt.Errorf("copier: get: lstat %q(%q): %w", oldItem, item, err)
1224+
}
1225+
followedLinks++
1226+
}
1227+
if followedLinks >= maxFollowedLinks {
1228+
return "", nil, fmt.Errorf("copier: get: resolving symlink %q(%q): %w", oldItem, item, syscall.ELOOP)
1229+
}
1230+
return item, info, nil
1231+
}
1232+
11851233
func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
11861234
statRequest := req
11871235
statRequest.Request = requestStat
@@ -1196,15 +1244,25 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
11961244
return errorResponse("copier: get: expected at least one glob pattern, got 0")
11971245
}
11981246
// build a queue of items by globbing
1199-
var queue []string
1247+
type queueItem struct {
1248+
glob string
1249+
parents []string
1250+
}
1251+
var queue []queueItem
12001252
globMatchedCount := 0
12011253
for _, glob := range req.Globs {
12021254
globMatched, err := extendedGlob(glob)
12031255
if err != nil {
12041256
return errorResponse("copier: get: glob %q: %v", glob, err)
12051257
}
1206-
globMatchedCount += len(globMatched)
1207-
queue = append(queue, globMatched...)
1258+
for _, path := range globMatched {
1259+
var parents []string
1260+
if req.GetOptions.Parents {
1261+
parents = getParents(path, req.Directory)
1262+
}
1263+
globMatchedCount++
1264+
queue = append(queue, queueItem{glob: path, parents: parents})
1265+
}
12081266
}
12091267
// no matches -> error
12101268
if len(queue) == 0 {
@@ -1219,7 +1277,8 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
12191277
defer tw.Close()
12201278
hardlinkChecker := new(hardlinkChecker)
12211279
itemsCopied := 0
1222-
for i, item := range queue {
1280+
for i, qItem := range queue {
1281+
item := qItem.glob
12231282
// if we're not discarding the names of individual directories, keep track of this one
12241283
relNamePrefix := ""
12251284
if req.GetOptions.KeepDirectoryNames {
@@ -1230,31 +1289,47 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
12301289
if err != nil {
12311290
return fmt.Errorf("copier: get: lstat %q: %w", item, err)
12321291
}
1233-
// chase links. if we hit a dead end, we should just fail
1234-
followedLinks := 0
1235-
const maxFollowedLinks = 16
1236-
for !req.GetOptions.NoDerefSymlinks && info.Mode()&os.ModeType == os.ModeSymlink && followedLinks < maxFollowedLinks {
1237-
path, err := os.Readlink(item)
1292+
if req.GetOptions.Parents && info.Mode().IsDir() {
1293+
if !slices.Contains(qItem.parents, item) {
1294+
qItem.parents = append(qItem.parents, item)
1295+
}
1296+
}
1297+
// Copy parents in to tarball first if exists
1298+
for _, parent := range qItem.parents {
1299+
oldParent := parent
1300+
parentInfo, err := os.Lstat(parent)
12381301
if err != nil {
1239-
continue
1302+
return fmt.Errorf("copier: get: lstat %q: %w", parent, err)
12401303
}
1241-
if filepath.IsAbs(path) || looksLikeAbs(path) {
1242-
path = filepath.Join(req.Root, path)
1243-
} else {
1244-
path = filepath.Join(filepath.Dir(item), path)
1304+
parent, parentInfo, err = checkLinks(parent, req, parentInfo)
1305+
if err != nil {
1306+
return err
1307+
}
1308+
parentName, err := convertToRelSubdirectory(req.Directory, oldParent)
1309+
if err != nil {
1310+
return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", parent, req.Directory, err)
12451311
}
1246-
item = path
1247-
if _, err = convertToRelSubdirectory(req.Root, item); err != nil {
1248-
return fmt.Errorf("copier: get: computing path of %q(%q) relative to %q: %w", queue[i], item, req.Root, err)
1312+
if parentName == "" || parentName == "." {
1313+
// skip the "." entry
1314+
continue
12491315
}
1250-
if info, err = os.Lstat(item); err != nil {
1251-
return fmt.Errorf("copier: get: lstat %q(%q): %w", queue[i], item, err)
1316+
if err := copierHandlerGetOne(parentInfo, "", parentName, parent, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil {
1317+
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
1318+
continue
1319+
} else if errors.Is(err, os.ErrNotExist) {
1320+
logrus.Warningf("copier: file disappeared while reading: %q", parent)
1321+
return nil
1322+
}
1323+
return fmt.Errorf("copier: get: %q: %w", queue[i].glob, err)
12521324
}
1253-
followedLinks++
1325+
itemsCopied++
12541326
}
1255-
if followedLinks >= maxFollowedLinks {
1256-
return fmt.Errorf("copier: get: resolving symlink %q(%q): %w", queue[i], item, syscall.ELOOP)
1327+
1328+
item, info, err = checkLinks(item, req, info)
1329+
if err != nil {
1330+
return err
12571331
}
1332+
12581333
// evaluate excludes relative to the root directory
12591334
if info.Mode().IsDir() {
12601335
// we don't expand any of the contents that are archives
@@ -1354,6 +1429,12 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13541429
ok = filepath.SkipDir
13551430
}
13561431
}
1432+
if req.GetOptions.Parents {
1433+
rel, err = convertToRelSubdirectory(req.Directory, path)
1434+
if err != nil {
1435+
return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", path, req.Root, err)
1436+
}
1437+
}
13571438
// add the item to the outgoing tar stream
13581439
if err := copierHandlerGetOne(info, symlinkTarget, rel, path, options, tw, hardlinkChecker, idMappings); err != nil {
13591440
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
@@ -1368,7 +1449,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13681449
}
13691450
// walk the directory tree, checking/adding items individually
13701451
if err := filepath.WalkDir(item, walkfn); err != nil {
1371-
return fmt.Errorf("copier: get: %q(%q): %w", queue[i], item, err)
1452+
return fmt.Errorf("copier: get: %q(%q): %w", queue[i].glob, item, err)
13721453
}
13731454
itemsCopied++
13741455
} else {
@@ -1379,15 +1460,24 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13791460
if skip {
13801461
continue
13811462
}
1382-
// add the item to the outgoing tar stream. in
1383-
// cases where this was a symlink that we
1384-
// dereferenced, be sure to use the name of the
1385-
// link.
1386-
if err := copierHandlerGetOne(info, "", filepath.Base(queue[i]), item, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil {
1463+
1464+
name := filepath.Base(queue[i].glob)
1465+
if req.GetOptions.Parents {
1466+
name, err = convertToRelSubdirectory(req.Directory, queue[i].glob)
1467+
if err != nil {
1468+
return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", item, req.Root, err)
1469+
}
1470+
if name == "" || name == "." {
1471+
// skip the "." entry
1472+
continue
1473+
}
1474+
}
1475+
1476+
if err := copierHandlerGetOne(info, "", name, item, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil {
13871477
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
13881478
continue
13891479
}
1390-
return fmt.Errorf("copier: get: %q: %w", queue[i], err)
1480+
return fmt.Errorf("copier: get: %q: %w", queue[i].glob, err)
13911481
}
13921482
itemsCopied++
13931483
}

0 commit comments

Comments
 (0)