Skip to content

Commit b812d45

Browse files
author
Joshua Arrevillaga
committed
Added --include flag to buildah add and buildah copy
Complement to existing --exclude flag. When specified, only files matching the given patterns are copied. Uses the same containerignore(5) pattern format. Can be combined with --exclude, where exclude takes priority. Signed-off-by: Joshua Arrevillaga <2004jarevillaga@gmail.com>
1 parent 08c63af commit b812d45

7 files changed

Lines changed: 192 additions & 14 deletions

File tree

add.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ type AddAndCopyOptions struct {
126126
// FollowSymlink controls whether symlinks should be followed when copying content.
127127
// When set to false, symlinks are not dereferenced.
128128
FollowSymlink types.OptionalBool
129+
// Includes is a list of patterns to include, the complement to Excludes.
130+
// Only items matching one of these patterns are copied. Has the same
131+
// pattern format as lines of a .containerignore file.
132+
Includes []string
129133
}
130134

131135
// getURL writes a tar archive containing the named content
@@ -608,6 +612,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
608612
UIDMap: srcUIDMap,
609613
GIDMap: srcGIDMap,
610614
Excludes: options.Excludes,
615+
Includes: options.Includes,
611616
ExpandArchives: extract,
612617
Chmod: options.Chmod,
613618
ChownDirs: chownDirs,
@@ -774,6 +779,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
774779
UIDMap: srcUIDMap,
775780
GIDMap: srcGIDMap,
776781
Excludes: options.Excludes,
782+
Includes: options.Includes,
777783
ExpandArchives: extract,
778784
Chmod: options.Chmod,
779785
ChownDirs: chownDirs,

cmd/buildah/addcopy.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type addCopyResults struct {
4646
allowWildcard bool
4747
allowEmptyWildcard bool
4848
noFollowSymlinks bool
49+
includes []string
4950
}
5051

5152
func createCommand(addCopy string, desc string, short string, opts *addCopyResults) *cobra.Command {
@@ -108,6 +109,7 @@ func applyFlagVars(flags *pflag.FlagSet, opts *addCopyResults) {
108109
flags.StringVar(&opts.timestamp, "timestamp", "", "set timestamps on new content to `seconds` after the epoch")
109110
flags.BoolVar(&opts.allowWildcard, "allow-wildcard", true, "allow glob patterns in source paths")
110111
flags.BoolVar(&opts.allowEmptyWildcard, "allow-empty-wildcard", false, "don't error when glob patterns match nothing")
112+
flags.StringSliceVar(&opts.includes, "include", nil, "include pattern when copying files")
111113
}
112114

113115
func addcopyInit() {
@@ -297,6 +299,7 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe
297299
Timestamp: timestamp,
298300
Link: iopts.link,
299301
FollowSymlink: followSymlink,
302+
Includes: iopts.includes,
300303
}
301304
if iopts.contextdir != "" {
302305
var excludes []string

copier/copier.go

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@ func (req *request) Excludes() []string {
198198
}
199199
}
200200

201+
func (req *request) Includes() []string {
202+
switch req.Request {
203+
case requestGet:
204+
return req.GetOptions.Includes
205+
default:
206+
return nil
207+
}
208+
}
209+
201210
func (req *request) UIDMap() []idtools.IDMap {
202211
switch req.Request {
203212
case requestEval:
@@ -405,6 +414,7 @@ type GetOptions struct {
405414
Timestamp *time.Time // timestamp to force on all contents
406415
DisallowWildcard bool // reject glob patterns in source paths
407416
AllowEmptyWildcard bool // don't error when glob patterns match nothing
417+
Includes []string // include only contents matching at least one of these patterns; Excludes take precedence
408418
}
409419

410420
// Get produces an archive containing items that match the specified glob
@@ -1072,11 +1082,19 @@ func copierHandler(bulkReader io.Reader, bulkWriter io.Writer, req request) (*re
10721082
// os.PathSeparator, implying that it expects OS-specific naming
10731083
// conventions.
10741084
excludes := req.Excludes()
1075-
pm, err := fileutils.NewPatternMatcher(excludes)
1085+
pmExcludes, err := fileutils.NewPatternMatcher(excludes)
10761086
if err != nil {
10771087
return nil, nil, fmt.Errorf("processing excludes list %v: %w", excludes, err)
10781088
}
10791089

1090+
var pmIncludes *fileutils.PatternMatcher
1091+
if includes := req.Includes(); len(includes) > 0 {
1092+
pmIncludes, err = fileutils.NewPatternMatcher(includes)
1093+
if err != nil {
1094+
return nil, nil, fmt.Errorf("processing includes list %v: %w", includes, err)
1095+
}
1096+
}
1097+
10801098
var idMappings *idtools.IDMappings
10811099
uidMap, gidMap := req.UIDMap(), req.GIDMap()
10821100
if len(uidMap) > 0 && len(gidMap) > 0 {
@@ -1090,10 +1108,10 @@ func copierHandler(bulkReader io.Reader, bulkWriter io.Writer, req request) (*re
10901108
resp := copierHandlerEval(req)
10911109
return resp, nil, nil
10921110
case requestStat:
1093-
resp := copierHandlerStat(req, pm, idMappings)
1111+
resp := copierHandlerStat(req, pmExcludes, idMappings)
10941112
return resp, nil, nil
10951113
case requestGet:
1096-
return copierHandlerGet(bulkWriter, req, pm, idMappings)
1114+
return copierHandlerGet(bulkWriter, req, pmExcludes, pmIncludes, idMappings)
10971115
case requestPut:
10981116
return copierHandlerPut(bulkReader, req, idMappings)
10991117
case requestMkdir:
@@ -1140,6 +1158,28 @@ func pathIsExcluded(root, path string, pm *fileutils.PatternMatcher) (string, bo
11401158
return rel, false, nil
11411159
}
11421160

1161+
func pathIsIncluded(root, path string, pm *fileutils.PatternMatcher) (bool, error) {
1162+
rel, err := convertToRelSubdirectory(root, path)
1163+
if err != nil {
1164+
return false, fmt.Errorf("copier: error computing path of %q relative to root %q: %w", path, root, err)
1165+
}
1166+
if pm == nil {
1167+
return true, nil
1168+
}
1169+
if rel == "." {
1170+
// special case
1171+
return true, nil
1172+
}
1173+
// Matches uses filepath.FromSlash() to convert candidates before
1174+
// checking if they match the patterns it's been given, implying that
1175+
// it expects Unix-style paths.
1176+
matches, err := pm.Matches(filepath.ToSlash(rel)) //nolint:staticcheck
1177+
if err != nil {
1178+
return false, fmt.Errorf("copier: error checking if %q is included: %w", rel, err)
1179+
}
1180+
return matches, nil
1181+
}
1182+
11431183
// resolvePath resolves symbolic links in paths, treating the specified
11441184
// directory as the root.
11451185
// Resolving the path this way, and using the result, is in no way secure
@@ -1221,7 +1261,7 @@ func containsWildcards(path string) bool {
12211261
return strings.ContainsAny(path, "*?[")
12221262
}
12231263

1224-
func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response {
1264+
func copierHandlerStat(req request, pmExcludes *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response {
12251265
errorResponse := func(fmtspec string, args ...any) *response {
12261266
return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse{}}
12271267
}
@@ -1250,7 +1290,7 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *id
12501290
s.Globbed = make([]string, 0, len(globMatched))
12511291
s.Results = make(map[string]*StatForItem)
12521292
for _, globbed := range globMatched {
1253-
rel, excluded, err := pathIsExcluded(req.Root, globbed, pm)
1293+
rel, excluded, err := pathIsExcluded(req.Root, globbed, pmExcludes)
12541294
if err != nil {
12551295
return errorResponse("copier: stat: %v", err)
12561296
}
@@ -1310,7 +1350,7 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *id
13101350
// could be a relative link) and in the context
13111351
// of the chroot
13121352
result.ImmediateTarget = immediateTarget
1313-
resolvedTarget, err := resolvePath(req.Root, globbed, true, pm)
1353+
resolvedTarget, err := resolvePath(req.Root, globbed, true, pmExcludes)
13141354
if err != nil {
13151355
return errorResponse("copier: stat: error resolving %q: %v", globbed, err)
13161356
}
@@ -1403,8 +1443,8 @@ func checkLinks(item string, req request, info os.FileInfo) (string, os.FileInfo
14031443
return item, info, nil
14041444
}
14051445

1406-
func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
1407-
statResponse := copierHandlerStat(req, pm, idMappings)
1446+
func copierHandlerGet(bulkWriter io.Writer, req request, pmExcludes, pmIncludes *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
1447+
statResponse := copierHandlerStat(req, pmExcludes, idMappings)
14081448
errorResponse := func(fmtspec string, args ...any) (*response, func() error, error) {
14091449
return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse.Stat, Get: getResponse{}}, nil, nil
14101450
}
@@ -1570,7 +1610,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
15701610
// skip the "." entry
15711611
return nil
15721612
}
1573-
skippedPath, skip, err := pathIsExcluded(req.Root, path, pm)
1613+
skippedPath, skip, err := pathIsExcluded(req.Root, path, pmExcludes)
15741614
if err != nil {
15751615
return err
15761616
}
@@ -1581,14 +1621,14 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
15811621
// all, we don't need to
15821622
// descend into this particular
15831623
// directory if it's a directory
1584-
if !pm.Exclusions() {
1624+
if !pmExcludes.Exclusions() {
15851625
return filepath.SkipDir
15861626
}
15871627
// if there are exclusion
15881628
// patterns for which this
15891629
// path is a prefix, we
15901630
// need to keep descending
1591-
for _, pattern := range pm.Patterns() {
1631+
for _, pattern := range pmExcludes.Patterns() {
15921632
if !pattern.Exclusion() {
15931633
continue
15941634
}
@@ -1611,6 +1651,16 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
16111651
// also be in the excludes list
16121652
return nil
16131653
}
1654+
if pmIncludes != nil && !d.IsDir() {
1655+
included, err := pathIsIncluded(item, path, pmIncludes)
1656+
if err != nil {
1657+
return err
1658+
}
1659+
1660+
if !included {
1661+
return nil
1662+
}
1663+
}
16141664
// if it's a symlink, read its target
16151665
symlinkTarget := ""
16161666
if d.Type() == os.ModeSymlink {
@@ -1656,14 +1706,24 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
16561706
}
16571707
itemsCopied++
16581708
} else {
1659-
_, skip, err := pathIsExcluded(req.Root, item, pm)
1709+
_, skip, err := pathIsExcluded(req.Root, item, pmExcludes)
16601710
if err != nil {
16611711
return err
16621712
}
16631713
if skip {
16641714
continue
16651715
}
16661716

1717+
if pmIncludes != nil {
1718+
included, err := pathIsIncluded(req.Root, item, pmIncludes)
1719+
if err != nil {
1720+
return err
1721+
}
1722+
if !included {
1723+
continue
1724+
}
1725+
}
1726+
16671727
name := filepath.Base(queue[i].glob)
16681728
if req.GetOptions.Parents {
16691729
name, err = convertToRelSubdirectory(req.Directory, queue[i].glob)

docs/buildah-add.1.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ by symbolic links outside of the chroot will fail.
6262

6363
**--exclude** *pattern*
6464

65-
Exclude copying files matching the specified pattern. Option can be specified
65+
Exclude copying files matching the specified pattern. The option can be specified
6666
multiple times. Patterns are matched against each file's path relative to the
6767
context directory (or, with **--from**, relative to the source container or image root).
6868
See containerignore(5) for supported formats.
@@ -78,6 +78,13 @@ can be used.
7878

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

81+
**--include** *pattern*
82+
83+
Only copy files matching the specified pattern. The option can be specified multiple times.
84+
Patterns are matched against each file's path relative to the source directory being copied.
85+
If a path matches both an **--include** and an **--exclude** pattern, it will be excluded.
86+
See containerignore(5) for supported formats.
87+
8188
**--link**
8289

8390
Create an independent image layer for the added files instead of modifying the working

docs/buildah-copy.1.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ by symbolic links outside of the chroot will fail.
6060

6161
**--exclude** *pattern*
6262

63-
Exclude copying files matching the specified pattern. Option can be specified
63+
Exclude copying files matching the specified pattern. The option can be specified
6464
multiple times. Patterns are matched against each file's path relative to the
6565
context directory (or, with **--from**, relative to the source container or image root).
6666
See containerignore(5) for supported formats.
@@ -77,6 +77,13 @@ is preserved.
7777

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

80+
**--include** *pattern*
81+
82+
Only copy files matching the specified pattern. The option can be specified multiple times.
83+
Patterns are matched against each file's path relative to the source directory being copied.
84+
If a path matches both an **--include** and an **--exclude** pattern, it will be excluded.
85+
See containerignore(5) for supported formats.
86+
8087
**--link**
8188

8289
Create an independent image layer for the added files instead of modifying the working

tests/add.bats

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,3 +668,56 @@ EOF
668668
cmp $ubuntu/etc/passwd ${croot}/tmp/passwd
669669
cmp $ubuntu/etc/passwd ${croot}/tmp/passwd2
670670
}
671+
672+
@test "add --include" {
673+
mytest=${TEST_SCRATCH_DIR}/mytest
674+
mkdir -p ${mytest}/subdir
675+
touch ${mytest}/source.go
676+
touch ${mytest}/readme.md
677+
touch ${mytest}/subdir/nested.go
678+
touch ${mytest}/subdir/nested.md
679+
680+
expect="
681+
stuff
682+
stuff/source.go
683+
stuff/subdir
684+
stuff/subdir/nested.go"
685+
686+
run_buildah from $WITH_POLICY_JSON scratch
687+
cid=$output
688+
run_buildah add --include="**/*.go" $cid ${mytest} /stuff
689+
690+
run_buildah_mount $cid
691+
mnt=$output
692+
run find $mnt -printf "%P\n"
693+
filelist=$(LC_ALL=C sort <<<"$output")
694+
run_buildah_umount $cid
695+
expect_output --from="$filelist" "$expect" "add recursive include"
696+
697+
run_buildah from $WITH_POLICY_JSON scratch
698+
cid=$output
699+
run_buildah add --include="**/*.go" $cid ${mytest}/source.go /stuff2/
700+
701+
run_buildah_mount $cid
702+
mnt=$output
703+
run find $mnt -printf "%P\n"
704+
filelist=$(LC_ALL=C sort <<<"$output")
705+
run_buildah_umount $cid
706+
expect_output --from="$filelist" --substring "source.go" "add include single file"
707+
708+
# include + exclude: exclude wins when both match
709+
expect="
710+
stuff
711+
stuff/source.go
712+
stuff/subdir"
713+
714+
run_buildah from $WITH_POLICY_JSON scratch
715+
cid=$output
716+
run_buildah add --include="**/*.go" --exclude="**/nested.go" $cid ${mytest} /stuff
717+
run_buildah_mount $cid
718+
mnt=$output
719+
run find $mnt -printf "%P\n"
720+
filelist=$(LC_ALL=C sort <<<"$output")
721+
run_buildah_umount $cid
722+
expect_output --from="$filelist" "$expect" "include with exclude"
723+
}

tests/copy.bats

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,3 +807,45 @@ parents/y/b.txt"
807807
run_buildah 125 copy --allow-empty-wildcard=true $cid ${TEST_SCRATCH_DIR}/no-such-file /dest4/
808808
expect_output --substring "no such file or directory"
809809
}
810+
811+
@test "copy --include" {
812+
mytest=${TEST_SCRATCH_DIR}/mytest
813+
mkdir -p ${mytest}/subdir
814+
touch ${mytest}/source.go
815+
touch ${mytest}/readme.md
816+
touch ${mytest}/subdir/nested.go
817+
touch ${mytest}/subdir/nested.md
818+
819+
# recursive include: **/*.go keeps .go at all depths, drops .md
820+
expect="
821+
stuff
822+
stuff/source.go
823+
stuff/subdir
824+
stuff/subdir/nested.go"
825+
826+
run_buildah from $WITH_POLICY_JSON scratch
827+
cid=$output
828+
run_buildah copy --include="**/*.go" $cid ${mytest} /stuff
829+
run_buildah_mount $cid
830+
mnt=$output
831+
run find $mnt -printf "%P\n"
832+
filelist=$(LC_ALL=C sort <<<"$output")
833+
run_buildah_umount $cid
834+
expect_output --from="$filelist" "$expect" "recursive include"
835+
836+
# include + exclude: exclude wins when both match
837+
expect="
838+
stuff
839+
stuff/source.go
840+
stuff/subdir"
841+
842+
run_buildah from $WITH_POLICY_JSON scratch
843+
cid=$output
844+
run_buildah copy --include="**/*.go" --exclude="**/nested.go" $cid ${mytest} /stuff
845+
run_buildah_mount $cid
846+
mnt=$output
847+
run find $mnt -printf "%P\n"
848+
filelist=$(LC_ALL=C sort <<<"$output")
849+
run_buildah_umount $cid
850+
expect_output --from="$filelist" "$expect" "include with exclude"
851+
}

0 commit comments

Comments
 (0)