From c3f527f57d2a5ec9525a1f51073086bb458c7069 Mon Sep 17 00:00:00 2001 From: Fatih Akca Date: Mon, 11 May 2026 16:32:44 +0100 Subject: [PATCH] add/copy: support AllowWildcard and AllowEmptyWildcard Expose --allow-wildcard and --allow-empty-wildcard on buildah add/copy, aligning with BuildKit's FileActionCopy flags. --allow-wildcard (default true) controls whether source paths may contain glob characters. When false, wildcards are rejected. --allow-empty-wildcard (default false) controls whether zero glob matches is an error. Individual empty wildcards are silently skipped, operation only fails when every source matches nothing. Signed-off-by: Fatih Akca --- add.go | 18 ++++- cmd/buildah/addcopy.go | 59 ++++++++------ copier/copier.go | 56 +++++++++---- copier/copier_test.go | 158 ++++++++++++++++++++++++++++++++++++- copier/copier_unix_test.go | 40 ++++++++++ docs/buildah-add.1.md | 14 ++++ docs/buildah-copy.1.md | 14 ++++ tests/add.bats | 53 +++++++++++++ tests/bud.bats | 2 +- tests/copy.bats | 53 +++++++++++++ 10 files changed, 426 insertions(+), 41 deletions(-) diff --git a/add.go b/add.go index 266ef7d801e..cc7eb5e8a14 100644 --- a/add.go +++ b/add.go @@ -115,6 +115,12 @@ type AddAndCopyOptions struct { // DirCopyContents copies the directory's contents instead of the // directory with its contents below it, default true. DirCopyContents types.OptionalBool + // AllowWildcard controls whether glob patterns are allowed in source + // paths. When false, they are rejected. Defaults to true. + AllowWildcard types.OptionalBool + // AllowEmptyWildcard controls whether the operation succeeds when all + // glob patterns match nothing. Defaults to false. + AllowEmptyWildcard types.OptionalBool } // gitURLFragmentSuffix matches fragments to use as Git reference and build @@ -378,7 +384,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption var localSourceStats []*copier.StatsForGlob if len(localSources) > 0 { statOptions := copier.StatOptions{ - CheckForArchives: extract, + CheckForArchives: extract, + DisallowWildcard: options.AllowWildcard == types.OptionalBoolFalse, + AllowEmptyWildcard: options.AllowEmptyWildcard == types.OptionalBoolTrue, } localSourceStats, err = copier.Stat(contextDir, contextDir, statOptions, localSources) if err != nil { @@ -399,11 +407,17 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption return fmt.Errorf("checking on sources under %q: %v", contextDir, errorText) } if len(localSourceStat.Globbed) == 0 { + if options.AllowEmptyWildcard == types.OptionalBoolTrue { + continue + } return fmt.Errorf("checking source under %q: no glob matches: %w", contextDir, syscall.ENOENT) } numLocalSourceItems += len(localSourceStat.Globbed) } if numLocalSourceItems+len(remoteSources)+len(gitSources) == 0 { + if options.AllowEmptyWildcard == types.OptionalBoolTrue { + return nil + } return fmt.Errorf("no sources %v found: %w", sources, syscall.ENOENT) } @@ -795,6 +809,8 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption StripStickyBit: options.StripStickyBit, Parents: options.Parents, Timestamp: options.Timestamp, + DisallowWildcard: options.AllowWildcard == types.OptionalBoolFalse, + AllowEmptyWildcard: options.AllowEmptyWildcard == types.OptionalBoolTrue, } getErr = copier.Get(contextDir, contextDir, getOptions, []string{globbedToGlobbable(globbed)}, writer) closeErr = writer.Close() diff --git a/cmd/buildah/addcopy.go b/cmd/buildah/addcopy.go index cfa8ebf8703..1df6877bff8 100644 --- a/cmd/buildah/addcopy.go +++ b/cmd/buildah/addcopy.go @@ -16,32 +16,35 @@ import ( "go.podman.io/buildah/pkg/cli" "go.podman.io/buildah/pkg/parse" "go.podman.io/common/pkg/auth" + "go.podman.io/image/v5/types" "go.podman.io/storage" ) type addCopyResults struct { - addHistory bool - chmod string - chown string - checksum string - quiet bool - ignoreFile string - contextdir string - from string - blobCache string - decryptionKeys []string - removeSignatures bool - signaturePolicy string - authfile string - creds string - tlsVerify bool - certDir string - retry int - retryDelay string - excludes []string - parents bool - timestamp string - link bool + addHistory bool + chmod string + chown string + checksum string + quiet bool + ignoreFile string + contextdir string + from string + blobCache string + decryptionKeys []string + removeSignatures bool + signaturePolicy string + authfile string + creds string + tlsVerify bool + certDir string + retry int + retryDelay string + excludes []string + parents bool + timestamp string + link bool + allowWildcard bool + allowEmptyWildcard bool } func createCommand(addCopy string, desc string, short string, opts *addCopyResults) *cobra.Command { @@ -101,6 +104,8 @@ func applyFlagVars(flags *pflag.FlagSet, opts *addCopyResults) { panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err)) } flags.StringVar(&opts.timestamp, "timestamp", "", "set timestamps on new content to `seconds` after the epoch") + flags.BoolVar(&opts.allowWildcard, "allow-wildcard", true, "allow glob patterns in source paths") + flags.BoolVar(&opts.allowEmptyWildcard, "allow-empty-wildcard", false, "don't error when glob patterns match nothing") } func addcopyInit() { @@ -277,6 +282,12 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe } options.Excludes = append(excludes, options.Excludes...) } + if c.Flags().Changed("allow-wildcard") { + options.AllowWildcard = types.NewOptionalBool(iopts.allowWildcard) + } + if c.Flags().Changed("allow-empty-wildcard") { + options.AllowEmptyWildcard = types.NewOptionalBool(iopts.allowEmptyWildcard) + } if iopts.retryDelay != "" { retryDelay, err := time.ParseDuration(iopts.retryDelay) if err != nil { @@ -307,6 +318,10 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe } contentType, digest := builder.ContentDigester.Digest() + if digest == "" { + logrus.Debug("no content copied, skipping digest and history") + return nil + } if !iopts.quiet { fmt.Printf("%s\n", digest.Hex()) } diff --git a/copier/copier.go b/copier/copier.go index 048585d9b2f..91fc50914c3 100644 --- a/copier/copier.go +++ b/copier/copier.go @@ -344,9 +344,11 @@ func Eval(root string, directory string, _ EvalOptions) (string, error) { // StatOptions controls parts of Stat()'s behavior. type StatOptions struct { - UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs when returning results - CheckForArchives bool // check for and populate the IsArchive bit in returned values - Excludes []string // contents to pretend don't exist, using the OS-specific path separator + UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs when returning results + CheckForArchives bool // check for and populate the IsArchive bit in returned values + Excludes []string // contents to pretend don't exist, using the OS-specific path separator + DisallowWildcard bool // reject glob patterns in source paths + AllowEmptyWildcard bool // don't error when glob patterns match nothing } // Stat globs the specified pattern in the specified directory and returns its @@ -399,6 +401,8 @@ type GetOptions struct { IgnoreUnreadable bool // ignore errors reading items, instead of returning an error NoCrossDevice bool // if a subdirectory is a mountpoint with a different device number, include it but skip its contents Timestamp *time.Time // timestamp to force on all contents + DisallowWildcard bool // reject glob patterns in source paths + AllowEmptyWildcard bool // don't error when glob patterns match nothing } // Get produces an archive containing items that match the specified glob @@ -420,7 +424,9 @@ func Get(root string, directory string, options GetOptions, globs []string, bulk Directory: directory, Globs: slices.Clone(globs), StatOptions: StatOptions{ - CheckForArchives: options.ExpandArchives, + CheckForArchives: options.ExpandArchives, + DisallowWildcard: options.DisallowWildcard, + AllowEmptyWildcard: options.AllowEmptyWildcard, }, GetOptions: options, } @@ -1108,6 +1114,10 @@ func copierHandlerEval(req request) *response { return &response{Eval: evalResponse{Evaluated: filepath.Join(req.rootPrefix, resolvedTarget)}} } +func containsWildcards(path string) bool { + return strings.ContainsAny(path, "*?[") +} + func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response { errorResponse := func(fmtspec string, args ...any) *response { return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse{}} @@ -1120,13 +1130,17 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *id s := StatsForGlob{ Glob: req.preservedGlobs[i], } - // glob this pattern + hasWildcards := containsWildcards(glob) + if req.StatOptions.DisallowWildcard && hasWildcards { + s.Error = fmt.Sprintf("copier: stat: %q: wildcards are not allowed", glob) + stats = append(stats, &s) + continue + } globMatched, err := extendedGlob(glob) if err != nil { s.Error = fmt.Sprintf("copier: stat: %q while matching glob pattern %q", err.Error(), glob) } - - if len(globMatched) == 0 && strings.ContainsAny(glob, "*?[") { + if len(globMatched) == 0 && hasWildcards { continue } // collect the matches @@ -1218,18 +1232,18 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *id result.IsArchive = isArchivePath(globbed) } } - // no unskipped matches -> error if len(s.Globbed) == 0 { s.Globbed = nil s.Results = nil - s.Error = fmt.Sprintf("copier: stat: %q: %v", glob, syscall.ENOENT) + if !hasWildcards || !req.StatOptions.AllowEmptyWildcard { + s.Error = fmt.Sprintf("copier: stat: %q: %v", glob, syscall.ENOENT) + } } stats = append(stats, &s) } - // no matches -> error - if len(stats) == 0 { + if len(stats) == 0 && !req.StatOptions.AllowEmptyWildcard { s := StatsForGlob{ - Error: fmt.Sprintf("copier: stat: %q: %v", req.Globs, syscall.ENOENT), + Error: fmt.Sprintf("copier: stat: globs %v matched nothing", req.Globs), } stats = append(stats, &s) } @@ -1307,10 +1321,20 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa var queue []queueItem globMatchedCount := 0 for _, glob := range req.Globs { + hasWildcards := containsWildcards(glob) + if req.GetOptions.DisallowWildcard && hasWildcards { + return errorResponse("copier: get: %q: wildcards are not allowed", glob) + } globMatched, err := extendedGlob(glob) if err != nil { return errorResponse("copier: get: glob %q: %v", glob, err) } + if len(globMatched) == 0 { + if hasWildcards { + continue + } + return errorResponse("copier: get: %q: %v", glob, syscall.ENOENT) + } for _, path := range globMatched { var parents []string if req.GetOptions.Parents { @@ -1320,9 +1344,11 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa queue = append(queue, queueItem{glob: path, parents: parents}) } } - // no matches -> error if len(queue) == 0 { - return errorResponse("copier: get: globs %v matched nothing (%d filtered out): %v", req.Globs, globMatchedCount, syscall.ENOENT) + if req.GetOptions.AllowEmptyWildcard { + return &response{Stat: statResponse.Stat, Get: getResponse{}}, nil, nil + } + return errorResponse("copier: get: globs %v matched nothing (%d filtered out)", req.Globs, globMatchedCount) } topInfo, err := os.Stat(req.Directory) if err != nil { @@ -1555,7 +1581,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa itemsCopied++ } } - if itemsCopied == 0 { + if itemsCopied == 0 && !req.GetOptions.AllowEmptyWildcard { return fmt.Errorf("copier: get: copied no items: %w", syscall.ENOENT) } return nil diff --git a/copier/copier_test.go b/copier/copier_test.go index 926bdafc210..d9aeded2bde 100644 --- a/copier/copier_test.go +++ b/copier/copier_test.go @@ -939,8 +939,8 @@ func testGetMultiple(t *testing.T) { }, expectedGetErrors: []expectedError{ {inSubdir: true, name: ".", err: syscall.ENOENT}, - {inSubdir: true, name: "/subdir-b/*", err: syscall.ENOENT}, - {inSubdir: true, name: "../../subdir-b/*", err: syscall.ENOENT}, + {inSubdir: true, name: "/subdir-b/*", err: fmt.Errorf("matched nothing")}, + {inSubdir: true, name: "../../subdir-b/*", err: fmt.Errorf("matched nothing")}, }, cases: []getTestArchiveCase{ { @@ -2365,6 +2365,160 @@ func testEnsure(t *testing.T) { } } +func TestStatDisallowWildcardNoChroot(t *testing.T) { + couldChroot := canChroot + canChroot = false + testStatDisallowWildcard(t) + canChroot = couldChroot +} + +func testStatDisallowWildcard(t *testing.T) { + headers := []tar.Header{ + {Name: "file-a", Typeflag: tar.TypeReg, Size: 23, Mode: 0o644, ModTime: testDate}, + {Name: "file-b", Typeflag: tar.TypeReg, Size: 45, Mode: 0o644, ModTime: testDate}, + {Name: "subdir", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: testDate}, + {Name: "subdir/file-c", Typeflag: tar.TypeReg, Size: 67, Mode: 0o644, ModTime: testDate}, + } + dir, err := makeContextFromArchive(t, makeArchive(headers, nil), "") + require.NoError(t, err) + + t.Run("wildcard-allowed-glob-matches", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{}, []string{"file-*"}) + require.NoError(t, err) + require.Len(t, stats, 1) + require.Empty(t, stats[0].Error) + require.Len(t, stats[0].Globbed, 2) + }) + + t.Run("disallow-wildcard-literal", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{DisallowWildcard: true}, []string{"file-a"}) + require.NoError(t, err) + require.Len(t, stats, 1) + require.Empty(t, stats[0].Error) + require.Len(t, stats[0].Globbed, 1) + }) + + t.Run("disallow-wildcard-glob-chars-rejected", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{DisallowWildcard: true}, []string{"file-*"}) + require.NoError(t, err) + require.Len(t, stats, 1) + require.NotEmpty(t, stats[0].Error, "file-* should be rejected when DisallowWildcard is true") + require.Contains(t, stats[0].Error, "wildcards are not allowed") + }) +} + +func TestStatAllowEmptyWildcardNoChroot(t *testing.T) { + couldChroot := canChroot + canChroot = false + testStatAllowEmptyWildcard(t) + canChroot = couldChroot +} + +func testStatAllowEmptyWildcard(t *testing.T) { + headers := []tar.Header{ + {Name: "file-a", Typeflag: tar.TypeReg, Size: 23, Mode: 0o644, ModTime: testDate}, + } + dir, err := makeContextFromArchive(t, makeArchive(headers, nil), "") + require.NoError(t, err) + + t.Run("empty-wildcard-true-no-error", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{AllowEmptyWildcard: true}, []string{"nonexistent-*"}) + require.NoError(t, err) + require.Empty(t, stats) + }) + + t.Run("empty-wildcard-false-error", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{}, []string{"nonexistent-*"}) + require.NoError(t, err) + require.Len(t, stats, 1) + require.Contains(t, stats[0].Error, "matched nothing") + }) + + t.Run("empty-wildcard-true-with-existing", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{AllowEmptyWildcard: true}, []string{"file-*"}) + require.NoError(t, err) + require.Len(t, stats, 1) + require.Empty(t, stats[0].Error) + require.Len(t, stats[0].Globbed, 1) + }) + + t.Run("empty-wildcard-true-literal-missing", func(t *testing.T) { + stats, err := Stat(dir, dir, StatOptions{AllowEmptyWildcard: true}, []string{"nonexistent-file"}) + require.NoError(t, err) + require.Len(t, stats, 1) + require.Contains(t, stats[0].Error, "no such file or directory") + }) +} + +func TestGetDisallowWildcardNoChroot(t *testing.T) { + couldChroot := canChroot + canChroot = false + testGetDisallowWildcard(t) + canChroot = couldChroot +} + +func testGetDisallowWildcard(t *testing.T) { + headers := []tar.Header{ + {Name: "file-a", Typeflag: tar.TypeReg, Size: 23, Mode: 0o644, ModTime: testDate}, + {Name: "file-b", Typeflag: tar.TypeReg, Size: 45, Mode: 0o644, ModTime: testDate}, + } + dir, err := makeContextFromArchive(t, makeArchive(headers, nil), "") + require.NoError(t, err) + + t.Run("wildcard-allowed-glob-matches", func(t *testing.T) { + err := Get(dir, dir, GetOptions{}, []string{"file-*"}, io.Discard) + require.NoError(t, err) + }) + + t.Run("disallow-wildcard-literal", func(t *testing.T) { + err := Get(dir, dir, GetOptions{DisallowWildcard: true}, []string{"file-a"}, io.Discard) + require.NoError(t, err) + }) + + t.Run("disallow-wildcard-glob-chars-rejected", func(t *testing.T) { + err := Get(dir, dir, GetOptions{DisallowWildcard: true}, []string{"file-*"}, io.Discard) + require.Error(t, err) + require.Contains(t, err.Error(), "wildcards are not allowed") + }) +} + +func TestGetAllowEmptyWildcardNoChroot(t *testing.T) { + couldChroot := canChroot + canChroot = false + testGetAllowEmptyWildcard(t) + canChroot = couldChroot +} + +func testGetAllowEmptyWildcard(t *testing.T) { + headers := []tar.Header{ + {Name: "file-a", Typeflag: tar.TypeReg, Size: 23, Mode: 0o644, ModTime: testDate}, + } + dir, err := makeContextFromArchive(t, makeArchive(headers, nil), "") + require.NoError(t, err) + + t.Run("empty-wildcard-true-no-error", func(t *testing.T) { + err := Get(dir, dir, GetOptions{AllowEmptyWildcard: true}, []string{"nonexistent-*"}, io.Discard) + require.NoError(t, err) + }) + + t.Run("empty-wildcard-false-error", func(t *testing.T) { + err := Get(dir, dir, GetOptions{}, []string{"nonexistent-*"}, io.Discard) + require.Error(t, err) + require.Contains(t, err.Error(), "matched nothing") + }) + + t.Run("empty-wildcard-true-literal-missing", func(t *testing.T) { + err := Get(dir, dir, GetOptions{AllowEmptyWildcard: true}, []string{"nonexistent-file"}, io.Discard) + require.Error(t, err) + require.Contains(t, err.Error(), "no such file or directory") + }) + + t.Run("empty-wildcard-true-with-existing", func(t *testing.T) { + err := Get(dir, dir, GetOptions{AllowEmptyWildcard: true}, []string{"file-*"}, io.Discard) + require.NoError(t, err) + }) +} + func TestEnsureNoChroot(t *testing.T) { couldChroot := canChroot canChroot = false diff --git a/copier/copier_unix_test.go b/copier/copier_unix_test.go index 2da316660c1..789cc94cf92 100644 --- a/copier/copier_unix_test.go +++ b/copier/copier_unix_test.go @@ -94,6 +94,46 @@ func TestEnsureChroot(t *testing.T) { canChroot = couldChroot } +func TestStatDisallowWildcardChroot(t *testing.T) { + if uid != 0 { + t.Skip("chroot() requires root privileges, skipping") + } + couldChroot := canChroot + canChroot = true + testStatDisallowWildcard(t) + canChroot = couldChroot +} + +func TestStatAllowEmptyWildcardChroot(t *testing.T) { + if uid != 0 { + t.Skip("chroot() requires root privileges, skipping") + } + couldChroot := canChroot + canChroot = true + testStatAllowEmptyWildcard(t) + canChroot = couldChroot +} + +func TestGetDisallowWildcardChroot(t *testing.T) { + if uid != 0 { + t.Skip("chroot() requires root privileges, skipping") + } + couldChroot := canChroot + canChroot = true + testGetDisallowWildcard(t) + canChroot = couldChroot +} + +func TestGetAllowEmptyWildcardChroot(t *testing.T) { + if uid != 0 { + t.Skip("chroot() requires root privileges, skipping") + } + couldChroot := canChroot + canChroot = true + testGetAllowEmptyWildcard(t) + canChroot = couldChroot +} + func TestConditionalRemoveChroot(t *testing.T) { if uid != 0 { t.Skip("chroot() requires root privileges, skipping") diff --git a/docs/buildah-add.1.md b/docs/buildah-add.1.md index e13e3040db1..0b99e03d83f 100644 --- a/docs/buildah-add.1.md +++ b/docs/buildah-add.1.md @@ -23,6 +23,16 @@ Defaults to false. Note: You can also override the default value of --add-history by setting the BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true` +**--allow-empty-wildcard** + +If set to true, don't return an error if globbing matches nothing. Only +meaningful when **--allow-wildcard** is true. Defaults to false. + +**--allow-wildcard** + +Allow glob patterns in source paths. When set to false, source paths containing +wildcard characters (*, ?, [) are rejected with an error. Defaults to true. + **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) when connecting to @@ -126,6 +136,10 @@ buildah add containerID 'https://github.com/containers/buildah/blob/main/README. buildah add containerID 'passwd' 'certs.d' /etc +buildah add containerID '/tmp/myfiles-*' '/dest/' + +buildah add --allow-empty-wildcard=true containerID '/tmp/logs-*' '/dest/' + ## FILES ### .containerignore or .dockerignore diff --git a/docs/buildah-copy.1.md b/docs/buildah-copy.1.md index 834aeab9b67..62f92cb9b3e 100644 --- a/docs/buildah-copy.1.md +++ b/docs/buildah-copy.1.md @@ -21,6 +21,16 @@ Defaults to false. Note: You can also override the default value of --add-history by setting the BUILDAH\_HISTORY environment variable. `export BUILDAH_HISTORY=true` +**--allow-empty-wildcard** + +If set to true, don't return an error if globbing matches nothing. Only +meaningful when **--allow-wildcard** is true. Defaults to false. + +**--allow-wildcard** + +Allow glob patterns in source paths. When set to false, source paths containing +wildcard characters (*, ?, [) are rejected with an error. Defaults to true. + **--cert-dir** *path* Use certificates at *path* (\*.crt, \*.cert, \*.key) when connecting to @@ -133,6 +143,10 @@ buildah copy containerID 'https://github.com/containers/buildah' '/tmp' buildah copy containerID 'passwd' 'certs.d' /etc +buildah copy containerID '/tmp/myfiles-*' '/dest/' + +buildah copy --allow-empty-wildcard=true containerID '/tmp/logs-*' '/dest/' + ## FILES ### .containerignore/.dockerignore diff --git a/tests/add.bats b/tests/add.bats index 6dd2600b73b..3c261be7431 100644 --- a/tests/add.bats +++ b/tests/add.bats @@ -509,3 +509,56 @@ EOF cmp ${TEST_SCRATCH_DIR}/testdir/file1 $newroot/testdir/file1 cmp ${TEST_SCRATCH_DIR}/testdir/subdir/file2 $newroot/testdir/subdir/file2 } + +@test "add-allow-wildcard" { + createrandom ${TEST_SCRATCH_DIR}/file-a + createrandom ${TEST_SCRATCH_DIR}/file-b + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + run_buildah config --workingdir / $cid + + # Default (allow-wildcard=true): glob expands, adds both files + run_buildah add $cid "${TEST_SCRATCH_DIR}/file-*" /dest/ + run_buildah mount $cid + root=$output + test -f $root/dest/file-a + test -f $root/dest/file-b + cmp ${TEST_SCRATCH_DIR}/file-a $root/dest/file-a + cmp ${TEST_SCRATCH_DIR}/file-b $root/dest/file-b + run_buildah umount $cid + + # allow-wildcard=false: glob patterns are rejected + run_buildah 125 add --allow-wildcard=false $cid "${TEST_SCRATCH_DIR}/file-*" /dest2/ + expect_output --substring "wildcards are not allowed" +} + +@test "add-allow-empty-wildcard" { + createrandom ${TEST_SCRATCH_DIR}/file-a + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + run_buildah config --workingdir / $cid + + # Default (allow-empty-wildcard=false): non-matching glob should fail + run_buildah 125 add $cid "${TEST_SCRATCH_DIR}/nonexistent-*" /dest/ + expect_output --substring "matched nothing" + + # Explicit true: non-matching glob should succeed + run_buildah add --allow-empty-wildcard=true $cid "${TEST_SCRATCH_DIR}/nonexistent-*" /dest2/ + run_buildah mount $cid + root=$output + test ! -d $root/dest2 + run_buildah umount $cid + + # Existing files still add normally + run_buildah add $cid ${TEST_SCRATCH_DIR}/file-a /dest3/ + run_buildah mount $cid + root=$output + test -f $root/dest3/file-a + run_buildah umount $cid + + # Literal non-existent file should still error even with allow-empty-wildcard=true + run_buildah 125 add --allow-empty-wildcard=true $cid ${TEST_SCRATCH_DIR}/no-such-file /dest4/ + expect_output --substring "no such file or directory" +} diff --git a/tests/bud.bats b/tests/bud.bats index d7b9f71af84..0e594d2eef5 100644 --- a/tests/bud.bats +++ b/tests/bud.bats @@ -6883,7 +6883,7 @@ _EOF run_buildah build -t test2 -f Containerfile.missing $WITH_POLICY_JSON $BUDFILES/copy-globs run_buildah 125 build -t test3 -f Containerfile.bad $WITH_POLICY_JSON $BUDFILES/copy-globs - expect_output --substring 'building.*"COPY \*foo /testdir".*no such file or directory' + expect_output --substring "matched nothing" } @test "bud with copy --exclude" { diff --git a/tests/copy.bats b/tests/copy.bats index abd39d5cbc4..1e748ac82a5 100644 --- a/tests/copy.bats +++ b/tests/copy.bats @@ -756,3 +756,56 @@ parents/y/b.txt" run_buildah run $newcid ls -l /tmp/random expect_output --substring "rwxrwxrwx" } + +@test "copy-allow-wildcard" { + createrandom ${TEST_SCRATCH_DIR}/file-a + createrandom ${TEST_SCRATCH_DIR}/file-b + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + run_buildah config --workingdir / $cid + + # Default (allow-wildcard=true): glob expands, copies both files + run_buildah copy $cid "${TEST_SCRATCH_DIR}/file-*" /dest/ + run_buildah mount $cid + root=$output + test -f $root/dest/file-a + test -f $root/dest/file-b + cmp ${TEST_SCRATCH_DIR}/file-a $root/dest/file-a + cmp ${TEST_SCRATCH_DIR}/file-b $root/dest/file-b + run_buildah umount $cid + + # allow-wildcard=false: glob patterns are rejected + run_buildah 125 copy --allow-wildcard=false $cid "${TEST_SCRATCH_DIR}/file-*" /dest2/ + expect_output --substring "wildcards are not allowed" +} + +@test "copy-allow-empty-wildcard" { + createrandom ${TEST_SCRATCH_DIR}/file-a + + run_buildah from $WITH_POLICY_JSON scratch + cid=$output + run_buildah config --workingdir / $cid + + # Default (allow-empty-wildcard=false): non-matching glob should fail + run_buildah 125 copy $cid "${TEST_SCRATCH_DIR}/nonexistent-*" /dest/ + expect_output --substring "matched nothing" + + # Explicit true: non-matching glob should succeed + run_buildah copy --allow-empty-wildcard=true $cid "${TEST_SCRATCH_DIR}/nonexistent-*" /dest2/ + run_buildah mount $cid + root=$output + test ! -d $root/dest2 + run_buildah umount $cid + + # Existing files still copy normally + run_buildah copy $cid ${TEST_SCRATCH_DIR}/file-a /dest3/ + run_buildah mount $cid + root=$output + test -f $root/dest3/file-a + run_buildah umount $cid + + # Literal non-existent file should still error even with allow-empty-wildcard=true + run_buildah 125 copy --allow-empty-wildcard=true $cid ${TEST_SCRATCH_DIR}/no-such-file /dest4/ + expect_output --substring "no such file or directory" +}