6363)
6464from .exceptions import InputException
6565from .context import trace_context
66+ import os
67+ import fnmatch
6668
6769__all__ = [
6870 "build_manifest" ,
@@ -315,6 +317,8 @@ class Options:
315317
316318 kustomize_flags : list [str ] = field (default_factory = list )
317319 skip_kustomize_path_validation : bool = False
320+ # Patterns passed to `flux build --ignore-paths` (may be repeated)
321+ ignore_paths : list [str ] = field (default_factory = list )
318322
319323
320324@dataclass
@@ -407,17 +411,21 @@ def adjust_ks_path(doc: Kustomization, selector: PathSelector) -> Path | None:
407411class CachableBuilder :
408412 """Wrapper around flux_build that caches contents."""
409413
410- def __init__ (self ) -> None :
414+ def __init__ (self , ignore_paths : list [ str ] | None = None ) -> None :
411415 """Initialize CachableBuilder."""
412416 self ._cache : dict [str , kustomize .Kustomize ] = {}
417+ self ._ignore_paths = list (ignore_paths or [])
413418
414419 async def build (
415420 self , kustomization : Kustomization , path : Path
416421 ) -> kustomize .Kustomize :
417- key = f"{ kustomization .namespaced_name } @ { path } "
422+ ignore_key = "," .join (sorted (self ._ignore_paths ))
423+ key = f"{ kustomization .namespaced_name } @ { path } | ignore=[{ ignore_key } ]"
418424 if cmd := self ._cache .get (key ):
419425 return cmd
420- cmd = kustomize .flux_build (kustomization , path )
426+ cmd = kustomize .flux_build (
427+ kustomization , path , ignore_paths = self ._ignore_paths
428+ )
421429 cmd = await cmd .stash ()
422430 self ._cache [key ] = cmd
423431 return cmd
@@ -466,10 +474,110 @@ async def visit_kustomization(
466474
467475 kinds = [CLUSTER_KUSTOMIZE_KIND , CONFIG_MAP_KIND , SECRET_KIND ]
468476
477+ def _normalize_patterns (patterns : list [str ]) -> list [str ]:
478+ out : list [str ] = []
479+ for p in patterns :
480+ for part in p .split ("," ):
481+ part = part .strip ()
482+ if part :
483+ out .append (part )
484+ return out
485+
486+ def _is_ignored (rel_path : Path , patterns : list [str ]) -> bool :
487+ # Convert to posix path for pattern matching
488+ s = rel_path .as_posix ()
489+ for pat in patterns :
490+ # Support trailing slash semantics from .gitignore (e.g., 'dir/')
491+ if pat .endswith ('/' ):
492+ base = pat [:- 1 ]
493+ if s == base or s .startswith (base + '/' ):
494+ return True
495+ if fnmatch .fnmatch (s , pat ):
496+ return True
497+ # Treat trailing '/**' patterns as also matching the directory itself
498+ if pat .endswith ('/**' ):
499+ base = pat [:- 3 ]
500+ if s == base or s .startswith (base + '/' ):
501+ return True
502+ return False
503+
504+ def _candidate_dirs (root : Path , patterns : list [str ]) -> list [Path ]:
505+ """Return directories likely containing Flux Kustomizations, honoring ignore patterns.
506+
507+ This scans YAML files textually for the Flux Kustomization apiVersion to avoid
508+ bulk YAML parsing errors from unrelated, non-Kubernetes YAML files.
509+ """
510+ candidates : set [Path ] = set ()
511+ norm = _normalize_patterns (patterns )
512+ for dirpath , dirnames , filenames in os .walk (root ):
513+ rel_dir = Path (dirpath ).resolve ().relative_to (root .resolve ())
514+ # Prune ignored subdirectories in-place for efficiency
515+ pruned = []
516+ for d in list (dirnames ):
517+ sub_rel = (rel_dir / d )
518+ if _is_ignored (sub_rel , norm ):
519+ pruned .append (d )
520+ for d in pruned :
521+ dirnames .remove (d )
522+
523+ # Skip this directory entirely if ignored
524+ if rel_dir != Path ('.' ) and _is_ignored (rel_dir , norm ):
525+ continue
526+
527+ for name in filenames :
528+ if not (name .endswith ('.yaml' ) or name .endswith ('.yml' )):
529+ continue
530+ file_rel = rel_dir / name
531+ if _is_ignored (file_rel , norm ):
532+ continue
533+ p = Path (dirpath ) / name
534+ try :
535+ with open (p , 'r' , encoding = 'utf-8' , errors = 'ignore' ) as f :
536+ # Read a chunk; we only need to detect a Flux Kustomization doc
537+ data = f .read (200000 )
538+ except OSError :
539+ continue
540+ if 'kustomize.toolkit.fluxcd.io' in data and 'kind: Kustomization' in data :
541+ candidates .add (Path (dirpath ))
542+ break
543+ return sorted (candidates )
544+
469545 with trace_context (f"Kustomization '{ label } '" ):
470546 cmd : kustomize .Kustomize
471547 if visit_ks is None :
472- cmd = kustomize .filter_resources (kinds , selector .root / path )
548+ # When ignore paths are provided, avoid scanning the entire tree at once.
549+ # Instead, limit grep to directories that appear to contain Flux Kustomizations.
550+ if options .ignore_paths :
551+ ks_docs : list [dict [str , Any ]] = []
552+ cfg_docs : list [dict [str , Any ]] = []
553+ root_path = selector .root / path
554+ for d in _candidate_dirs (root_path , options .ignore_paths ):
555+ sub_cmd = kustomize .filter_resources (kinds , d )
556+ sub_cmd = await sub_cmd .stash ()
557+ ks_cmd = sub_cmd .grep (GREP_SOURCE_REF_KIND )
558+ cfg_cmd = sub_cmd .filter_resources ([CONFIG_MAP_KIND , SECRET_KIND ])
559+ ks_docs .extend (await ks_cmd .objects ())
560+ cfg_docs .extend (await cfg_cmd .objects ())
561+ else :
562+ cmd = kustomize .filter_resources (kinds , selector .root / path )
563+ cmd = await cmd .stash ()
564+ ks_cmd = cmd .grep (GREP_SOURCE_REF_KIND )
565+ cfg_cmd = cmd .filter_resources ([CONFIG_MAP_KIND , SECRET_KIND ])
566+ try :
567+ ks_docs = await ks_cmd .objects ()
568+ cfg_docs = await cfg_cmd .objects ()
569+ except KustomizePathException as err :
570+ raise FluxException (err ) from err
571+ except FluxException as err :
572+ if visit_ks is None :
573+ raise FluxException (
574+ f"Error building Fluxtomization in '{ selector .root } ' "
575+ f"path '{ path } ': { ERROR_DETAIL_BAD_PATH } { err } "
576+ ) from err
577+ raise FluxException (
578+ f"Error building Fluxtomization '{ visit_ks .namespaced_name } ' "
579+ f"path '{ path } ': { ERROR_DETAIL_BAD_KS } { err } "
580+ ) from err
473581 else :
474582 if not await isdir (selector .root / path ):
475583 if options .skip_kustomize_path_validation :
@@ -484,25 +592,24 @@ async def visit_kustomization(
484592 )
485593 cmd = await builder .build (visit_ks , selector .root / path )
486594 cmd = cmd .filter_resources (kinds )
487- cmd = await cmd .stash ()
488- ks_cmd = cmd .grep (GREP_SOURCE_REF_KIND )
489- cfg_cmd = cmd .filter_resources ([CONFIG_MAP_KIND , SECRET_KIND ])
490-
491- try :
492- ks_docs = await ks_cmd .objects ()
493- cfg_docs = await cfg_cmd .objects ()
494- except KustomizePathException as err :
495- raise FluxException (err ) from err
496- except FluxException as err :
497- if visit_ks is None :
595+ cmd = await cmd .stash ()
596+ ks_cmd = cmd .grep (GREP_SOURCE_REF_KIND )
597+ cfg_cmd = cmd .filter_resources ([CONFIG_MAP_KIND , SECRET_KIND ])
598+ try :
599+ ks_docs = await ks_cmd .objects ()
600+ cfg_docs = await cfg_cmd .objects ()
601+ except KustomizePathException as err :
602+ raise FluxException (err ) from err
603+ except FluxException as err :
604+ if visit_ks is None :
605+ raise FluxException (
606+ f"Error building Fluxtomization in '{ selector .root } ' "
607+ f"path '{ path } ': { ERROR_DETAIL_BAD_PATH } { err } "
608+ ) from err
498609 raise FluxException (
499- f"Error building Fluxtomization in ' { selector . root } ' "
500- f"path '{ path } ': { ERROR_DETAIL_BAD_PATH } { err } "
610+ f"Error building Fluxtomization ' { visit_ks . namespaced_name } ' "
611+ f"path '{ path } ': { ERROR_DETAIL_BAD_KS } { err } "
501612 ) from err
502- raise FluxException (
503- f"Error building Fluxtomization '{ visit_ks .namespaced_name } ' "
504- f"path '{ path } ': { ERROR_DETAIL_BAD_KS } { err } "
505- ) from err
506613
507614 return VisitResult (
508615 kustomizations = list (
@@ -752,7 +859,7 @@ async def build_manifest(
752859 if not selector .cluster .enabled :
753860 return Manifest (clusters = [])
754861
755- builder = CachableBuilder ()
862+ builder = CachableBuilder (ignore_paths = options . ignore_paths )
756863
757864 with trace_context (f"Cluster '{ str (selector .path .path )} '" ):
758865 results = await kustomization_traversal (selector .path , builder , options )
0 commit comments