@@ -24,6 +24,7 @@ import (
2424 "path/filepath"
2525 "strings"
2626
27+ "github.com/bmatcuk/doublestar/v4"
2728 sourcev1 "github.com/fluxcd/source-controller/api/v1"
2829 "golang.org/x/mod/sumdb/dirhash"
2930 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -129,7 +130,7 @@ func applyCopyOperations(ctx context.Context,
129130// applyCopyOperation applies a single copy operation from the sources to the staging directory.
130131// This function implements cp-like semantics by first analyzing the source pattern to determine
131132// if it's a glob, direct file/directory reference, or wildcard pattern, then making copy decisions
132- // based on the actual source types found.
133+ // based on the actual source types found. Files matching exclude patterns are filtered out.
133134func applyCopyOperation (ctx context.Context ,
134135 op swapi.CopyOperation ,
135136 sources map [string ]string ,
@@ -149,6 +150,12 @@ func applyCopyOperation(ctx context.Context,
149150 return fmt .Errorf ("source alias '%s' not found" , srcAlias )
150151 }
151152
153+ for _ , pattern := range op .Exclude {
154+ if _ , err := doublestar .Match (pattern , "." ); err != nil {
155+ return fmt .Errorf ("invalid exclude pattern '%s'" , pattern )
156+ }
157+ }
158+
152159 // Create secure roots for file operations
153160 srcRoot , err := os .OpenRoot (srcDir )
154161 if err != nil {
@@ -168,7 +175,7 @@ func applyCopyOperation(ctx context.Context,
168175
169176 if ! isGlobPattern {
170177 // Direct path reference - check what it actually is first (cp-like behavior)
171- return applySingleSourceCopy (ctx , srcRoot , srcPattern , stagingRoot , destRelPath , destEndsWithSlash )
178+ return applySingleSourceCopy (ctx , srcRoot , srcPattern , stagingRoot , destRelPath , destEndsWithSlash , op . Exclude )
172179 }
173180
174181 // Glob pattern - find all matches and copy each
@@ -181,15 +188,27 @@ func applyCopyOperation(ctx context.Context,
181188 return fmt .Errorf ("no files match pattern '%s' in source '%s'" , srcPattern , srcAlias )
182189 }
183190
184- // For glob patterns, destination should be a directory (like cp *.txt dest/)
191+ // Filter out excluded files
192+ filteredMatches := make ([]string , 0 , len (matches ))
185193 for _ , match := range matches {
194+ if ! shouldExclude (match , op .Exclude ) {
195+ filteredMatches = append (filteredMatches , match )
196+ }
197+ }
198+
199+ if len (filteredMatches ) == 0 {
200+ return fmt .Errorf ("all files matching pattern '%s' in source '%s' were excluded" , srcPattern , srcAlias )
201+ }
202+
203+ // For glob patterns, destination should be a directory (like cp *.txt dest/)
204+ for _ , match := range filteredMatches {
186205 if err := ctx .Err (); err != nil {
187206 return err
188207 }
189208
190209 // Calculate destination path based on glob pattern type
191210 destFile := calculateGlobDestination (srcPattern , match , destRelPath )
192- if err := copyFileWithRoots (ctx , srcRoot , match , stagingRoot , destFile ); err != nil {
211+ if err := copyFileWithRoots (ctx , srcRoot , match , stagingRoot , destFile , op . Exclude ); err != nil {
193212 return fmt .Errorf ("failed to copy file '%s' to '%s': %w" , match , destFile , err )
194213 }
195214 }
@@ -204,7 +223,8 @@ func applySingleSourceCopy(ctx context.Context,
204223 srcPath string ,
205224 stagingRoot * os.Root ,
206225 destPath string ,
207- destEndsWithSlash bool ) error {
226+ destEndsWithSlash bool ,
227+ excludePatterns []string ) error {
208228 // Clean the source path to handle trailing slashes
209229 srcPath = filepath .Clean (srcPath )
210230
@@ -218,9 +238,9 @@ func applySingleSourceCopy(ctx context.Context,
218238 }
219239
220240 if srcInfo .IsDir () {
221- return applySingleDirectoryCopy (ctx , srcRoot , srcPath , stagingRoot , destPath )
241+ return applySingleDirectoryCopy (ctx , srcRoot , srcPath , stagingRoot , destPath , excludePatterns )
222242 } else {
223- return applySingleFileCopy (ctx , srcRoot , srcPath , stagingRoot , destPath , destEndsWithSlash )
243+ return applySingleFileCopy (ctx , srcRoot , srcPath , stagingRoot , destPath , destEndsWithSlash , excludePatterns )
224244 }
225245}
226246
@@ -232,7 +252,12 @@ func applySingleFileCopy(ctx context.Context,
232252 srcPath string ,
233253 stagingRoot * os.Root ,
234254 destPath string ,
235- destEndsWithSlash bool ) error {
255+ destEndsWithSlash bool ,
256+ excludePatterns []string ) error {
257+ // Check if the file should be excluded
258+ if shouldExclude (srcPath , excludePatterns ) {
259+ return nil // Skip excluded file
260+ }
236261 var finalDestPath string
237262
238263 if destEndsWithSlash {
@@ -250,7 +275,7 @@ func applySingleFileCopy(ctx context.Context,
250275 }
251276 }
252277
253- return copyFileWithRoots (ctx , srcRoot , srcPath , stagingRoot , finalDestPath )
278+ return copyFileWithRoots (ctx , srcRoot , srcPath , stagingRoot , finalDestPath , excludePatterns )
254279}
255280
256281// applySingleDirectoryCopy handles copying a single directory using cp-like semantics.
@@ -260,11 +285,12 @@ func applySingleDirectoryCopy(ctx context.Context,
260285 srcRoot * os.Root ,
261286 srcPath string ,
262287 stagingRoot * os.Root ,
263- destPath string ) error {
288+ destPath string ,
289+ excludePatterns []string ) error {
264290 srcDirName := filepath .Base (srcPath )
265291 finalDestPath := filepath .Join (destPath , srcDirName )
266292
267- return copyFileWithRoots (ctx , srcRoot , srcPath , stagingRoot , finalDestPath )
293+ return copyFileWithRoots (ctx , srcRoot , srcPath , stagingRoot , finalDestPath , excludePatterns )
268294}
269295
270296// containsGlobChars returns true if the path contains glob metacharacters
@@ -318,19 +344,21 @@ func parseCopyDestinationRelative(to string) (string, error) {
318344 return strings .TrimPrefix (to , "@artifact/" ), nil
319345}
320346
321- // copyFileWithRoots copies a file from srcRoot to stagingRoot os.Root.
347+ // copyFileWithRoots copies a file from srcRoot to stagingRoot os.Root,
348+ // excluding files matching exclude patterns.
322349func copyFileWithRoots (ctx context.Context ,
323350 srcRoot * os.Root ,
324351 srcPath string ,
325352 stagingRoot * os.Root ,
326- destPath string ) error {
353+ destPath string ,
354+ excludePatterns []string ) error {
327355 srcInfo , err := srcRoot .Stat (srcPath )
328356 if err != nil {
329357 return err
330358 }
331359
332360 if srcInfo .IsDir () {
333- return copyDirWithRoots (ctx , srcRoot , srcPath , stagingRoot , destPath )
361+ return copyDirWithRoots (ctx , srcRoot , srcPath , stagingRoot , destPath , excludePatterns )
334362 }
335363
336364 return copyRegularFileWithRoots (ctx , srcRoot , srcPath , stagingRoot , destPath )
@@ -383,12 +411,14 @@ func copyRegularFileWithRoots(ctx context.Context,
383411 return destFile .Chmod (srcInfo .Mode ())
384412}
385413
386- // copyDirWithRoots copies a directory recursively using os.Root.
414+ // copyDirWithRoots copies a directory recursively using os.Root,
415+ // skipping files and sub-dirs matching exclude patterns.
387416func copyDirWithRoots (ctx context.Context ,
388417 srcRoot * os.Root ,
389418 srcPath string ,
390419 stagingRoot * os.Root ,
391- destPath string ) error {
420+ destPath string ,
421+ excludePatterns []string ) error {
392422 return fs .WalkDir (srcRoot .FS (), srcPath , func (path string , d fs.DirEntry , err error ) error {
393423 if err := ctx .Err (); err != nil {
394424 return err
@@ -410,6 +440,16 @@ func copyDirWithRoots(ctx context.Context,
410440 return createDirRecursive (stagingRoot , destPath )
411441 }
412442
443+ // Check if this path should be excluded
444+ if shouldExclude (relPath , excludePatterns ) {
445+ if d .IsDir () {
446+ // Skip entire directory
447+ return fs .SkipDir
448+ }
449+ // Skip file
450+ return nil
451+ }
452+
413453 destFilePath := filepath .Join (destPath , relPath )
414454
415455 if d .IsDir () {
@@ -450,6 +490,23 @@ func createDirRecursive(root *os.Root, path string) error {
450490 return err
451491}
452492
493+ // shouldExclude checks if a path matches any of the exclude patterns.
494+ func shouldExclude (filePath string , excludePatterns []string ) bool {
495+ if len (excludePatterns ) == 0 {
496+ return false
497+ }
498+
499+ for _ , pattern := range excludePatterns {
500+ // We validate the patterns when parsing the copy operation,
501+ // so it's safe to use MatchUnvalidated here.
502+ if doublestar .MatchUnvalidated (pattern , filePath ) {
503+ return true
504+ }
505+ }
506+
507+ return false
508+ }
509+
453510// MkdirTempAbs creates a tmp dir and returns the absolute path to the dir.
454511// This is required since certain OSes like MacOS create temporary files in
455512// e.g. `/private/var`, to which `/var` is a symlink.
0 commit comments