Skip to content

Commit dd88ebb

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 0df9655 commit dd88ebb

7 files changed

Lines changed: 175 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 // contents to include, using the OS-specific path separator.
408418
}
409419

410420
// Get produces an archive containing items that match the specified glob
@@ -1024,11 +1034,19 @@ func copierHandler(bulkReader io.Reader, bulkWriter io.Writer, req request) (*re
10241034
// os.PathSeparator, implying that it expects OS-specific naming
10251035
// conventions.
10261036
excludes := req.Excludes()
1027-
pm, err := fileutils.NewPatternMatcher(excludes)
1037+
pmExcludes, err := fileutils.NewPatternMatcher(excludes)
10281038
if err != nil {
10291039
return nil, nil, fmt.Errorf("processing excludes list %v: %w", excludes, err)
10301040
}
10311041

1042+
var pmIncludes *fileutils.PatternMatcher
1043+
if includes := req.Includes(); len(includes) > 0 {
1044+
pmIncludes, err = fileutils.NewPatternMatcher(includes)
1045+
if err != nil {
1046+
return nil, nil, fmt.Errorf("processing includes list %v: %w", includes, err)
1047+
}
1048+
}
1049+
10321050
var idMappings *idtools.IDMappings
10331051
uidMap, gidMap := req.UIDMap(), req.GIDMap()
10341052
if len(uidMap) > 0 && len(gidMap) > 0 {
@@ -1042,10 +1060,10 @@ func copierHandler(bulkReader io.Reader, bulkWriter io.Writer, req request) (*re
10421060
resp := copierHandlerEval(req)
10431061
return resp, nil, nil
10441062
case requestStat:
1045-
resp := copierHandlerStat(req, pm, idMappings)
1063+
resp := copierHandlerStat(req, pmExcludes, idMappings)
10461064
return resp, nil, nil
10471065
case requestGet:
1048-
return copierHandlerGet(bulkWriter, req, pm, idMappings)
1066+
return copierHandlerGet(bulkWriter, req, pmExcludes, pmIncludes, idMappings)
10491067
case requestPut:
10501068
return copierHandlerPut(bulkReader, req, idMappings)
10511069
case requestMkdir:
@@ -1092,6 +1110,28 @@ func pathIsExcluded(root, path string, pm *fileutils.PatternMatcher) (string, bo
10921110
return rel, false, nil
10931111
}
10941112

1113+
func pathIsIncluded(root, path string, pm *fileutils.PatternMatcher) (bool, error) {
1114+
rel, err := convertToRelSubdirectory(root, path)
1115+
if err != nil {
1116+
return false, fmt.Errorf("copier: error computing path of %q relative to root %q: %w", path, root, err)
1117+
}
1118+
if pm == nil {
1119+
return true, nil
1120+
}
1121+
if rel == "." {
1122+
// special case
1123+
return true, nil
1124+
}
1125+
// Matches uses filepath.FromSlash() to convert candidates before
1126+
// checking if they match the patterns it's been given, implying that
1127+
// it expects Unix-style paths.
1128+
matches, err := pm.Matches(filepath.ToSlash(rel)) //nolint:staticcheck
1129+
if err != nil {
1130+
return false, fmt.Errorf("copier: error checking if %q is included: %w", rel, err)
1131+
}
1132+
return matches, nil
1133+
}
1134+
10951135
// resolvePath resolves symbolic links in paths, treating the specified
10961136
// directory as the root.
10971137
// Resolving the path this way, and using the result, is in no way secure
@@ -1173,7 +1213,7 @@ func containsWildcards(path string) bool {
11731213
return strings.ContainsAny(path, "*?[")
11741214
}
11751215

1176-
func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response {
1216+
func copierHandlerStat(req request, pmExcludes *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response {
11771217
errorResponse := func(fmtspec string, args ...any) *response {
11781218
return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse{}}
11791219
}
@@ -1202,7 +1242,7 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *id
12021242
s.Globbed = make([]string, 0, len(globMatched))
12031243
s.Results = make(map[string]*StatForItem)
12041244
for _, globbed := range globMatched {
1205-
rel, excluded, err := pathIsExcluded(req.Root, globbed, pm)
1245+
rel, excluded, err := pathIsExcluded(req.Root, globbed, pmExcludes)
12061246
if err != nil {
12071247
return errorResponse("copier: stat: %v", err)
12081248
}
@@ -1262,7 +1302,7 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *id
12621302
// could be a relative link) and in the context
12631303
// of the chroot
12641304
result.ImmediateTarget = immediateTarget
1265-
resolvedTarget, err := resolvePath(req.Root, globbed, true, pm)
1305+
resolvedTarget, err := resolvePath(req.Root, globbed, true, pmExcludes)
12661306
if err != nil {
12671307
return errorResponse("copier: stat: error resolving %q: %v", globbed, err)
12681308
}
@@ -1355,8 +1395,8 @@ func checkLinks(item string, req request, info os.FileInfo) (string, os.FileInfo
13551395
return item, info, nil
13561396
}
13571397

1358-
func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
1359-
statResponse := copierHandlerStat(req, pm, idMappings)
1398+
func copierHandlerGet(bulkWriter io.Writer, req request, pmExcludes, pmIncludes *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
1399+
statResponse := copierHandlerStat(req, pmExcludes, idMappings)
13601400
errorResponse := func(fmtspec string, args ...any) (*response, func() error, error) {
13611401
return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse.Stat, Get: getResponse{}}, nil, nil
13621402
}
@@ -1522,7 +1562,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
15221562
// skip the "." entry
15231563
return nil
15241564
}
1525-
skippedPath, skip, err := pathIsExcluded(req.Root, path, pm)
1565+
skippedPath, skip, err := pathIsExcluded(req.Root, path, pmExcludes)
15261566
if err != nil {
15271567
return err
15281568
}
@@ -1533,14 +1573,14 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
15331573
// all, we don't need to
15341574
// descend into this particular
15351575
// directory if it's a directory
1536-
if !pm.Exclusions() {
1576+
if !pmExcludes.Exclusions() {
15371577
return filepath.SkipDir
15381578
}
15391579
// if there are exclusion
15401580
// patterns for which this
15411581
// path is a prefix, we
15421582
// need to keep descending
1543-
for _, pattern := range pm.Patterns() {
1583+
for _, pattern := range pmExcludes.Patterns() {
15441584
if !pattern.Exclusion() {
15451585
continue
15461586
}
@@ -1563,6 +1603,16 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
15631603
// also be in the excludes list
15641604
return nil
15651605
}
1606+
if pmIncludes != nil && !d.IsDir() {
1607+
included, err := pathIsIncluded(item, path, pmIncludes)
1608+
if err != nil {
1609+
return err
1610+
}
1611+
1612+
if !included {
1613+
return nil
1614+
}
1615+
}
15661616
// if it's a symlink, read its target
15671617
symlinkTarget := ""
15681618
if d.Type() == os.ModeSymlink {
@@ -1608,14 +1658,24 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
16081658
}
16091659
itemsCopied++
16101660
} else {
1611-
_, skip, err := pathIsExcluded(req.Root, item, pm)
1661+
_, skip, err := pathIsExcluded(req.Root, item, pmExcludes)
16121662
if err != nil {
16131663
return err
16141664
}
16151665
if skip {
16161666
continue
16171667
}
16181668

1669+
if pmIncludes != nil {
1670+
included, err := pathIsIncluded(req.Root, item, pmIncludes)
1671+
if err != nil {
1672+
return err
1673+
}
1674+
if !included {
1675+
continue
1676+
}
1677+
}
1678+
16191679
name := filepath.Base(queue[i].glob)
16201680
if req.GetOptions.Parents {
16211681
name, err = convertToRelSubdirectory(req.Directory, queue[i].glob)

docs/buildah-add.1.md

Lines changed: 7 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,12 @@ 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 specific pattern, the complement of **--exclude**. The option
84+
can be specified multiple times. Patterns are matched against each file's path relative to
85+
the source directory being copied. See containerignore(5) for supported formats.
86+
8187
**--link**
8288

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

docs/buildah-copy.1.md

Lines changed: 7 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,12 @@ 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 specific pattern, the complement of **--exclude**. The option
83+
can be specified multiple times. Patterns are matched against each file's path relative to
84+
the source directory being copied. See containerignore(5) for supported formats.
85+
8086
**--link**
8187

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

tests/add.bats

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,3 +668,41 @@ 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+
# else arm: a single named file that matches include copies cleanly
698+
run_buildah from $WITH_POLICY_JSON scratch
699+
cid=$output
700+
run_buildah add --include=**/*.go $cid ${mytest}/source.go /stuff2/
701+
702+
run_buildah_mount $cid
703+
mnt=$output
704+
run find $mnt -printf "%P\n"
705+
filelist=$(LC_ALL=C sort <<<"$output")
706+
run_buildah_umount $cid
707+
expect_output --from="$filelist" --substring "source.go" "add include single file"
708+
}

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)