Skip to content

Commit 8c6ca1f

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 4c3081b commit 8c6ca1f

File tree

19 files changed

+301
-50
lines changed

19 files changed

+301
-50
lines changed

add.go

+42-23
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+
// ParentsPatterns are patterns to preserve parent directories of source content
98+
ParentsPatterns map[string]string
9799
}
98100

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

268+
// getParentsPrefixToRemove 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).
271+
// In case "/./" is not present is returned "/".
272+
func getParentsPrefixToRemove(pattern string) string {
273+
prefix, _, found := strings.Cut(pattern, "/./")
274+
if !found {
275+
return string(filepath.Separator)
276+
}
277+
return filepath.Clean(prefix)
278+
}
279+
266280
// Add copies the contents of the specified sources into the container's root
267281
// filesystem, optionally extracting contents of local files that look like
268282
// non-empty archives.
@@ -476,9 +490,12 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
476490
if err := copier.Mkdir(mountPoint, extractDirectory, mkdirOptions); err != nil {
477491
return fmt.Errorf("ensuring target directory exists: %w", err)
478492
}
479-
480493
// Copy each source in turn.
481494
for _, src := range sources {
495+
parentsPrefixToRemove := ""
496+
if pattern, ok := options.ParentsPatterns[src]; ok {
497+
parentsPrefixToRemove = getParentsPrefixToRemove(pattern)
498+
}
482499
var multiErr *multierror.Error
483500
var getErr, closeErr, renameErr, putErr error
484501
var wg sync.WaitGroup
@@ -498,17 +515,18 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
498515
var cloneDir, subdir string
499516
cloneDir, subdir, getErr = define.TempDirForURL(tmpdir.GetTempDir(), "", src)
500517
getOptions := copier.GetOptions{
501-
UIDMap: srcUIDMap,
502-
GIDMap: srcGIDMap,
503-
Excludes: options.Excludes,
504-
ExpandArchives: extract,
505-
ChownDirs: chownDirs,
506-
ChmodDirs: chmodDirsFiles,
507-
ChownFiles: chownFiles,
508-
ChmodFiles: chmodDirsFiles,
509-
StripSetuidBit: options.StripSetuidBit,
510-
StripSetgidBit: options.StripSetgidBit,
511-
StripStickyBit: options.StripStickyBit,
518+
UIDMap: srcUIDMap,
519+
GIDMap: srcGIDMap,
520+
Excludes: options.Excludes,
521+
ExpandArchives: extract,
522+
ParentsPrefixToRemove: parentsPrefixToRemove,
523+
ChownDirs: chownDirs,
524+
ChmodDirs: chmodDirsFiles,
525+
ChownFiles: chownFiles,
526+
ChmodFiles: chmodDirsFiles,
527+
StripSetuidBit: options.StripSetuidBit,
528+
StripSetgidBit: options.StripSetgidBit,
529+
StripStickyBit: options.StripStickyBit,
512530
}
513531
writer := io.WriteCloser(pipeWriter)
514532
repositoryDir := filepath.Join(cloneDir, subdir)
@@ -645,17 +663,18 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
645663
return false, false, nil
646664
})
647665
getOptions := copier.GetOptions{
648-
UIDMap: srcUIDMap,
649-
GIDMap: srcGIDMap,
650-
Excludes: options.Excludes,
651-
ExpandArchives: extract,
652-
ChownDirs: chownDirs,
653-
ChmodDirs: chmodDirsFiles,
654-
ChownFiles: chownFiles,
655-
ChmodFiles: chmodDirsFiles,
656-
StripSetuidBit: options.StripSetuidBit,
657-
StripSetgidBit: options.StripSetgidBit,
658-
StripStickyBit: options.StripStickyBit,
666+
UIDMap: srcUIDMap,
667+
GIDMap: srcGIDMap,
668+
Excludes: options.Excludes,
669+
ExpandArchives: extract,
670+
ChownDirs: chownDirs,
671+
ChmodDirs: chmodDirsFiles,
672+
ChownFiles: chownFiles,
673+
ChmodFiles: chmodDirsFiles,
674+
ParentsPrefixToRemove: parentsPrefixToRemove,
675+
StripSetuidBit: options.StripSetuidBit,
676+
StripSetgidBit: options.StripSetgidBit,
677+
StripStickyBit: options.StripStickyBit,
659678
}
660679
getErr = copier.Get(contextDir, contextDir, getOptions, []string{globbedToGlobbable(globbed)}, writer)
661680
closeErr = writer.Close()

cmd/buildah/addcopy.go

+8
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)
@@ -247,6 +249,12 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe
247249
InsecureSkipTLSVerify: systemContext.DockerInsecureSkipTLSVerify,
248250
MaxRetries: iopts.retry,
249251
}
252+
if iopts.parents {
253+
options.ParentsPatterns = map[string]string{}
254+
for _, pattern := range args {
255+
options.ParentsPatterns[pattern] = pattern
256+
}
257+
}
250258
if iopts.contextdir != "" {
251259
var excludes []string
252260

copier/copier.go

+74-22
Original file line numberDiff line numberDiff line change
@@ -343,22 +343,25 @@ func Stat(root string, directory string, options StatOptions, globs []string) ([
343343

344344
// GetOptions controls parts of Get()'s behavior.
345345
type GetOptions struct {
346-
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive
347-
Excludes []string // contents to pretend don't exist, using the OS-specific path separator
348-
ExpandArchives bool // extract the contents of named items that are archives
349-
ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted
350-
ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted
351-
ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted
352-
ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted
353-
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
354-
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
355-
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
356-
StripXattrs bool // don't record extended attributes of items being copied. no effect on archives being extracted
357-
KeepDirectoryNames bool // don't strip the top directory's basename from the paths of items in subdirectories
358-
Rename map[string]string // rename items with the specified names, or under the specified names
359-
NoDerefSymlinks bool // don't follow symlinks when globs match them
360-
IgnoreUnreadable bool // ignore errors reading items, instead of returning an error
361-
NoCrossDevice bool // if a subdirectory is a mountpoint with a different device number, include it but skip its contents
346+
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive
347+
Excludes []string // contents to pretend don't exist, using the OS-specific path separator
348+
ExpandArchives bool // extract the contents of named items that are archives
349+
ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted
350+
ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted
351+
ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted
352+
ChmodFiles *os.FileMode // set permissions on files. no effect on archives being extracted
353+
// ParentsPrefixToRemove is removed from source path and what is left is used as name to maintain
354+
// the sources parent directory in the destination if is empty is used only file name as name in tar.
355+
ParentsPrefixToRemove string
356+
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
357+
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
358+
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
359+
StripXattrs bool // don't record extended attributes of items being copied. no effect on archives being extracted
360+
KeepDirectoryNames bool // don't strip the top directory's basename from the paths of items in subdirectories
361+
Rename map[string]string // rename items with the specified names, or under the specified names
362+
NoDerefSymlinks bool // don't follow symlinks when globs match them
363+
IgnoreUnreadable bool // ignore errors reading items, instead of returning an error
364+
NoCrossDevice bool // if a subdirectory is a mountpoint with a different device number, include it but skip its contents
362365
}
363366

364367
// Get produces an archive containing items that match the specified glob
@@ -1215,6 +1218,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
12151218
return errorResponse("copier: get: error reading info about directory %q: %v", req.Directory, err)
12161219
}
12171220
cb := func() error {
1221+
alreadyCopied := map[string]struct{}{}
12181222
tw := tar.NewWriter(bulkWriter)
12191223
defer tw.Close()
12201224
hardlinkChecker := new(hardlinkChecker)
@@ -1354,6 +1358,9 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13541358
ok = filepath.SkipDir
13551359
}
13561360
}
1361+
if len(req.GetOptions.ParentsPrefixToRemove) > 0 {
1362+
rel = filepath.Clean(strings.TrimPrefix(path, filepath.Join(req.Directory, req.GetOptions.ParentsPrefixToRemove)))
1363+
}
13571364
// add the item to the outgoing tar stream
13581365
if err := copierHandlerGetOne(info, symlinkTarget, rel, path, options, tw, hardlinkChecker, idMappings); err != nil {
13591366
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
@@ -1379,17 +1386,37 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
13791386
if skip {
13801387
continue
13811388
}
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 {
1389+
1390+
copyFunc := func(name string, path string, fileInfo os.FileInfo) error {
1391+
// add the item to the outgoing tar stream. in
1392+
// cases where this was a symlink that we
1393+
// dereferenced, be sure to use the name of the
1394+
// link.
1395+
if err := copierHandlerGetOne(fileInfo, "", name, path, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil {
1396+
return err
1397+
}
1398+
itemsCopied++
1399+
return nil
1400+
}
1401+
1402+
name := filepath.Base(queue[i])
1403+
if len(req.GetOptions.ParentsPrefixToRemove) > 0 {
1404+
name = filepath.Clean(strings.TrimPrefix(item, filepath.Join(req.Directory, req.GetOptions.ParentsPrefixToRemove)))
1405+
alreadyCopied, err = copyParentsDirs(name, item, alreadyCopied, copyFunc)
1406+
if err != nil {
1407+
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
1408+
continue
1409+
}
1410+
return fmt.Errorf("copier: get: %q: %w", queue[i], err)
1411+
}
1412+
}
1413+
1414+
if err := copyFunc(name, item, info); err != nil {
13871415
if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) {
13881416
continue
13891417
}
13901418
return fmt.Errorf("copier: get: %q: %w", queue[i], err)
13911419
}
1392-
itemsCopied++
13931420
}
13941421
}
13951422
if itemsCopied == 0 {
@@ -1400,6 +1427,31 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
14001427
return &response{Stat: statResponse.Stat, Get: getResponse{}}, cb, nil
14011428
}
14021429

1430+
func copyParentsDirs(name string, path string, alreadyCopied map[string]struct{}, copy func(string, string, os.FileInfo) error) (map[string]struct{}, error) {
1431+
parentName := filepath.Dir(name)
1432+
parentPath := filepath.Dir(path)
1433+
for parentName != "/" && parentName != "." {
1434+
if _, ok := alreadyCopied[parentPath]; ok {
1435+
parentName = filepath.Dir(parentName)
1436+
parentPath = filepath.Dir(parentPath)
1437+
continue
1438+
}
1439+
1440+
parentInfo, err := os.Lstat(parentPath)
1441+
if err != nil {
1442+
return alreadyCopied, fmt.Errorf("copier: get: lstat %q: %w", parentPath, err)
1443+
}
1444+
if err := copy(strings.TrimPrefix(parentName, "/"), parentPath, parentInfo); err != nil {
1445+
return alreadyCopied, err
1446+
}
1447+
1448+
alreadyCopied[parentPath] = struct{}{}
1449+
parentName = filepath.Dir(parentName)
1450+
parentPath = filepath.Dir(parentPath)
1451+
}
1452+
return alreadyCopied, nil
1453+
}
1454+
14031455
func handleRename(rename map[string]string, name string) string {
14041456
if rename == nil {
14051457
return name

docs/buildah-copy.1.md

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ is preserved.
6565

6666
Path to an alternative .containerignore (.dockerignore) file. Requires \-\-contextdir be specified.
6767

68+
**--parents**
69+
70+
Preserve leading directories in the paths of items being copied, relative to either the
71+
top of the build context, or to the "pivot point", a location in the source path marked
72+
by a path component named "." (i.e., where "/./" occurs in the path).
73+
6874
**--quiet**, **-q**
6975

7076
Refrain from printing a digest of the copied content.
@@ -93,6 +99,8 @@ buildah copy containerID '/myapp/app.conf' '/myapp/app.conf'
9399

94100
buildah copy --exclude=**/*.md docs containerID 'docs' '/docs'
95101

102+
buildah copy --parents containerID './x/a.txt' './y/a.txt' '/parents'
103+
96104
buildah copy --chown myuser:mygroup containerID '/myapp/app.conf' '/myapp/app.conf'
97105

98106
buildah copy --chmod 660 containerID '/myapp/app.conf' '/myapp/app.conf'

imagebuildah/stage_executor.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,6 @@ func (s *StageExecutor) Copy(excludes []string, copies ...imagebuilder.Copy) err
368368
if cp.Link {
369369
return errors.New("COPY --link is not supported")
370370
}
371-
if cp.Parents {
372-
return errors.New("COPY --parents is not supported")
373-
}
374371
if len(cp.Excludes) > 0 {
375372
excludes = append(slices.Clone(excludes), cp.Excludes...)
376373
}
@@ -550,6 +547,7 @@ func (s *StageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
550547
} else {
551548
logrus.Debugf("COPY %#v, %#v", excludes, copy)
552549
}
550+
parentsPattern := map[string]string{}
553551
for _, src := range copy.Src {
554552
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
555553
// Source is a URL, allowed for ADD but not COPY.
@@ -560,7 +558,9 @@ func (s *StageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
560558
return fmt.Errorf("source can't be a URL for COPY")
561559
}
562560
} else {
563-
sources = append(sources, filepath.Join(contextDir, src))
561+
source := filepath.Join(contextDir, src)
562+
parentsPattern[source] = src
563+
sources = append(sources, source)
564564
}
565565
}
566566
options := buildah.AddAndCopyOptions{
@@ -582,6 +582,9 @@ func (s *StageExecutor) performCopy(excludes []string, copies ...imagebuilder.Co
582582
MaxRetries: s.executor.maxPullPushRetries,
583583
RetryDelay: s.executor.retryPullPushDelay,
584584
}
585+
if copy.Parents {
586+
options.ParentsPatterns = parentsPattern
587+
}
585588
if len(copy.Files) > 0 {
586589
// If we are copying heredoc files, we need to temporary place
587590
// them in the context dir and then move to container via copier

tests/bud.bats

+8-1
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ _EOF
394394
assert "$output" !~ "First symlink content"
395395

396396
# Modify the symlink
397-
ln -sf samplefile2 $contextdir/tomount
397+
ln -sf samplefile2 $contextdir/tomount
398398

399399
# on third run since we have changed symlink so cache must burst.
400400
run_buildah build $WITH_POLICY_JSON --layers -t source -f $contextdir/Containerfile $contextdir
@@ -6225,6 +6225,13 @@ _EOF
62256225
assert "$output" !~ "test2.txt"
62266226
}
62276227

6228+
@test "bud with copy --parents" {
6229+
run_buildah build -t test $WITH_POLICY_JSON $BUDFILES/copy-parents
6230+
assert "$output" !~ "./no_parents/a.txt
6231+
./parents/x/a.txt
6232+
./parents/y/a.txt"
6233+
}
6234+
62286235
@test "bud with containerfile secret" {
62296236
_prefetch alpine
62306237
mytmpdir=${TEST_SCRATCH_DIR}/my-dir1

tests/bud/copy-parents/Containerfile

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM alpine
2+
COPY ./x/a.txt ./y/a.txt /no_parents/
3+
COPY --parents ./x/a.txt ./y/a.txt /parents/
4+
run find /no_parents
5+
run find /parents

tests/bud/copy-parents/x/a.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A-FILE

tests/bud/copy-parents/x/y/a.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A-FILE

tests/bud/copy-parents/x/y/b.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello

tests/bud/copy-parents/y/a.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A-FILE

tests/bud/copy-parents/y/b.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello

0 commit comments

Comments
 (0)