Skip to content

Commit 7ca9f34

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 e8d8be7 commit 7ca9f34

File tree

25 files changed

+637
-36
lines changed

25 files changed

+637
-36
lines changed

add.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os"
1313
"path"
1414
"path/filepath"
15+
"slices"
1516
"strconv"
1617
"strings"
1718
"sync"
@@ -94,6 +95,8 @@ type AddAndCopyOptions struct {
9495
// RetryDelay is how long to wait before retrying attempts to retrieve
9596
// remote contents.
9697
RetryDelay time.Duration
98+
// Parents preserve parent directories of source content
99+
Parents bool
97100
}
98101

99102
// gitURLFragmentSuffix matches fragments to use as Git reference and build
@@ -263,6 +266,25 @@ func globbedToGlobbable(glob string) string {
263266
return result
264267
}
265268

269+
// getParentsPrefixToRemoveAndParentsToSkip gets from the pattern the prefix before the "pivot point",
270+
// the location in the source path marked by the path component named "."
271+
// (i.e. where "/./" occurs in the path). And list of parents to skip.
272+
// In case "/./" is not present is returned "/".
273+
func getParentsPrefixToRemoveAndParentsToSkip(pattern string, contextDir string) (string, []string) {
274+
prefix, _, found := strings.Cut(strings.TrimPrefix(pattern, contextDir), "/./")
275+
if !found {
276+
return string(filepath.Separator), []string{}
277+
}
278+
prefix = strings.TrimPrefix(filepath.Clean(string(filepath.Separator)+prefix), string(filepath.Separator))
279+
out := []string{}
280+
parentPath := prefix
281+
for parentPath != "/" && parentPath != "." {
282+
out = append(out, parentPath)
283+
parentPath = filepath.Dir(parentPath)
284+
}
285+
return prefix, out
286+
}
287+
266288
// Add copies the contents of the specified sources into the container's root
267289
// filesystem, optionally extracting contents of local files that look like
268290
// non-empty archives.
@@ -476,7 +498,6 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
476498
if err := copier.Mkdir(mountPoint, extractDirectory, mkdirOptions); err != nil {
477499
return fmt.Errorf("ensuring target directory exists: %w", err)
478500
}
479-
480501
// Copy each source in turn.
481502
for _, src := range sources {
482503
var multiErr *multierror.Error
@@ -587,7 +608,6 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
587608
if localSourceStat == nil {
588609
continue
589610
}
590-
591611
// Iterate through every item that matched the glob.
592612
itemsCopied := 0
593613
for _, globbed := range localSourceStat.Globbed {
@@ -640,6 +660,25 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
640660
return false, false, nil
641661
})
642662
}
663+
664+
if options.Parents {
665+
parentsPrefixToRemove, parentsToSkip := getParentsPrefixToRemoveAndParentsToSkip(src, options.ContextDir)
666+
writer = newTarFilterer(writer, func(hdr *tar.Header) (bool, bool, io.Reader) {
667+
if slices.Contains(parentsToSkip, hdr.Name) && hdr.Typeflag == tar.TypeDir {
668+
return true, false, nil
669+
}
670+
hdr.Name = strings.TrimPrefix(hdr.Name, parentsPrefixToRemove)
671+
hdr.Name = strings.TrimPrefix(hdr.Name, "/")
672+
if hdr.Typeflag == tar.TypeLink {
673+
hdr.Linkname = strings.TrimPrefix(hdr.Linkname, parentsPrefixToRemove)
674+
hdr.Linkname = strings.TrimPrefix(hdr.Linkname, "/")
675+
}
676+
if hdr.Name == "" {
677+
return true, false, nil
678+
}
679+
return false, false, nil
680+
})
681+
}
643682
writer = newTarFilterer(writer, func(_ *tar.Header) (bool, bool, io.Reader) {
644683
itemsCopied++
645684
return false, false, nil
@@ -656,6 +695,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
656695
StripSetuidBit: options.StripSetuidBit,
657696
StripSetgidBit: options.StripSetgidBit,
658697
StripStickyBit: options.StripStickyBit,
698+
Parents: options.Parents,
659699
}
660700
getErr = copier.Get(contextDir, contextDir, getOptions, []string{globbedToGlobbable(globbed)}, writer)
661701
closeErr = writer.Close()

cmd/buildah/addcopy.go

Lines changed: 3 additions & 0 deletions
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

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os/user"
1414
"path"
1515
"path/filepath"
16+
"slices"
1617
"strconv"
1718
"strings"
1819
"sync"
@@ -350,6 +351,7 @@ type GetOptions struct {
350351
ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted
351352
ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted
352353
ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted
354+
Parents bool // maintain the sources parent directory in the destination
353355
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
354356
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
355357
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
@@ -1182,6 +1184,49 @@ func errorIsPermission(err error) bool {
11821184
return errors.Is(err, os.ErrPermission) || strings.Contains(err.Error(), "permission denied")
11831185
}
11841186

1187+
func getParents(path string, stopPath string) []string {
1188+
out := []string{}
1189+
for path != "/" && path != "." && path != stopPath {
1190+
path = filepath.Dir(path)
1191+
if path == stopPath {
1192+
continue
1193+
}
1194+
out = append(out, path)
1195+
}
1196+
slices.Reverse(out)
1197+
return out
1198+
}
1199+
1200+
func checkLinks(item string, req request, info os.FileInfo) (string, os.FileInfo, error) {
1201+
// chase links. if we hit a dead end, we should just fail
1202+
oldItem := item
1203+
followedLinks := 0
1204+
const maxFollowedLinks = 16
1205+
for !req.GetOptions.NoDerefSymlinks && info.Mode()&os.ModeType == os.ModeSymlink && followedLinks < maxFollowedLinks {
1206+
path, err := os.Readlink(item)
1207+
if err != nil {
1208+
continue
1209+
}
1210+
if filepath.IsAbs(path) || looksLikeAbs(path) {
1211+
path = filepath.Join(req.Root, path)
1212+
} else {
1213+
path = filepath.Join(filepath.Dir(item), path)
1214+
}
1215+
item = path
1216+
if _, err = convertToRelSubdirectory(req.Root, item); err != nil {
1217+
return "", nil, fmt.Errorf("copier: get: computing path of %q(%q) relative to %q: %w", oldItem, item, req.Root, err)
1218+
}
1219+
if info, err = os.Lstat(item); err != nil {
1220+
return "", nil, fmt.Errorf("copier: get: lstat %q(%q): %w", oldItem, item, err)
1221+
}
1222+
followedLinks++
1223+
}
1224+
if followedLinks >= maxFollowedLinks {
1225+
return "", nil, fmt.Errorf("copier: get: resolving symlink %q(%q): %w", oldItem, item, syscall.ELOOP)
1226+
}
1227+
return item, info, nil
1228+
}
1229+
11851230
func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
11861231
statRequest := req
11871232
statRequest.Request = requestStat
@@ -1196,15 +1241,25 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
11961241
return errorResponse("copier: get: expected at least one glob pattern, got 0")
11971242
}
11981243
// build a queue of items by globbing
1199-
var queue []string
1244+
type queueItem struct {
1245+
glob string
1246+
parents []string
1247+
}
1248+
var queue []queueItem
12001249
globMatchedCount := 0
12011250
for _, glob := range req.Globs {
12021251
globMatched, err := extendedGlob(glob)
12031252
if err != nil {
12041253
return errorResponse("copier: get: glob %q: %v", glob, err)
12051254
}
1206-
globMatchedCount += len(globMatched)
1207-
queue = append(queue, globMatched...)
1255+
for _, path := range globMatched {
1256+
var parents []string
1257+
if req.GetOptions.Parents {
1258+
parents = getParents(path, req.Directory)
1259+
}
1260+
globMatchedCount++
1261+
queue = append(queue, queueItem{glob: path, parents: parents})
1262+
}
12081263
}
12091264
// no matches -> error
12101265
if len(queue) == 0 {
@@ -1219,7 +1274,9 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
12191274
defer tw.Close()
12201275
hardlinkChecker := new(hardlinkChecker)
12211276
itemsCopied := 0
1222-
for i, item := range queue {
1277+
addedParents := map[string]struct{}{}
1278+
for i, qItem := range queue {
1279+
item := qItem.glob
12231280
// if we're not discarding the names of individual directories, keep track of this one
12241281
relNamePrefix := ""
12251282
if req.GetOptions.KeepDirectoryNames {
@@ -1230,31 +1287,53 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
12301287
if err != nil {
12311288
return fmt.Errorf("copier: get: lstat %q: %w", item, err)
12321289
}
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)
1290+
if req.GetOptions.Parents && info.Mode().IsDir() {
1291+
if !slices.Contains(qItem.parents, item) {
1292+
qItem.parents = append(qItem.parents, item)
1293+
}
1294+
}
1295+
// Copy parents in to tarball first if exists
1296+
for _, parent := range qItem.parents {
1297+
oldParent := parent
1298+
parentInfo, err := os.Lstat(parent)
12381299
if err != nil {
1239-
continue
1300+
return fmt.Errorf("copier: get: lstat %q: %w", parent, err)
12401301
}
1241-
if filepath.IsAbs(path) || looksLikeAbs(path) {
1242-
path = filepath.Join(req.Root, path)
1243-
} else {
1244-
path = filepath.Join(filepath.Dir(item), path)
1302+
parent, parentInfo, err = checkLinks(parent, req, parentInfo)
1303+
if err != nil {
1304+
return err
1305+
}
1306+
parentName, err := convertToRelSubdirectory(req.Directory, oldParent)
1307+
if err != nil {
1308+
return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", parent, req.Directory, err)
12451309
}
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)
1310+
if parentName == "" || parentName == "." {
1311+
// skip the "." entry
1312+
continue
12491313
}
1250-
if info, err = os.Lstat(item); err != nil {
1251-
return fmt.Errorf("copier: get: lstat %q(%q): %w", queue[i], item, err)
1314+
1315+
if _, ok := addedParents[parentName]; ok {
1316+
continue
12521317
}
1253-
followedLinks++
1318+
addedParents[parentName] = struct{}{}
1319+
1320+
if err := copierHandlerGetOne(parentInfo, "", parentName, parent, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil {
1321+
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
1322+
continue
1323+
} else if errors.Is(err, os.ErrNotExist) {
1324+
logrus.Warningf("copier: file disappeared while reading: %q", parent)
1325+
return nil
1326+
}
1327+
return fmt.Errorf("copier: get: %q: %w", queue[i].glob, err)
1328+
}
1329+
itemsCopied++
12541330
}
1255-
if followedLinks >= maxFollowedLinks {
1256-
return fmt.Errorf("copier: get: resolving symlink %q(%q): %w", queue[i], item, syscall.ELOOP)
1331+
1332+
item, info, err = checkLinks(item, req, info)
1333+
if err != nil {
1334+
return err
12571335
}
1336+
12581337
// evaluate excludes relative to the root directory
12591338
if info.Mode().IsDir() {
12601339
// we don't expand any of the contents that are archives
@@ -1354,6 +1433,12 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13541433
ok = filepath.SkipDir
13551434
}
13561435
}
1436+
if req.GetOptions.Parents {
1437+
rel, err = convertToRelSubdirectory(req.Directory, path)
1438+
if err != nil {
1439+
return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", path, req.Root, err)
1440+
}
1441+
}
13571442
// add the item to the outgoing tar stream
13581443
if err := copierHandlerGetOne(info, symlinkTarget, rel, path, options, tw, hardlinkChecker, idMappings); err != nil {
13591444
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
@@ -1368,7 +1453,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13681453
}
13691454
// walk the directory tree, checking/adding items individually
13701455
if err := filepath.WalkDir(item, walkfn); err != nil {
1371-
return fmt.Errorf("copier: get: %q(%q): %w", queue[i], item, err)
1456+
return fmt.Errorf("copier: get: %q(%q): %w", queue[i].glob, item, err)
13721457
}
13731458
itemsCopied++
13741459
} else {
@@ -1379,15 +1464,24 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13791464
if skip {
13801465
continue
13811466
}
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 {
1467+
1468+
name := filepath.Base(queue[i].glob)
1469+
if req.GetOptions.Parents {
1470+
name, err = convertToRelSubdirectory(req.Directory, queue[i].glob)
1471+
if err != nil {
1472+
return fmt.Errorf("copier: get: error computing path of %q relative to %q: %w", item, req.Root, err)
1473+
}
1474+
if name == "" || name == "." {
1475+
// skip the "." entry
1476+
continue
1477+
}
1478+
}
1479+
1480+
if err := copierHandlerGetOne(info, "", name, item, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil {
13871481
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
13881482
continue
13891483
}
1390-
return fmt.Errorf("copier: get: %q: %w", queue[i], err)
1484+
return fmt.Errorf("copier: get: %q: %w", queue[i].glob, err)
13911485
}
13921486
itemsCopied++
13931487
}

0 commit comments

Comments
 (0)