Skip to content

Commit 4c3ae02

Browse files
committed
feat: add --ignore-paths support
Add `--ignore-paths` flag to CLI and actions. Some repositories may contain yaml files that are not kustomize or cluster resources. Allow skipping those paths. Fixes: #211
1 parent 8405195 commit 4c3ae02

File tree

12 files changed

+287
-26
lines changed

12 files changed

+287
-26
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ metadata.labels.helm.sh/chart (PersistentVolumeClaim/default/hajimari-data)
168168
+ hajimari-2.0.1
169169
```
170170

171+
Ignore specific paths
172+
173+
You can exclude non-Kubernetes or otherwise irrelevant files and directories during
174+
diff with the `--ignore-paths` flag. Patterns are `.gitignore`-style and may be
175+
comma-separated. A trailing slash matches a directory and all its contents (e.g., `.github/`).
176+
177+
```bash
178+
$ flux-local diff ks apps --path clusters/prod --ignore-paths ".github/,scripts/"
179+
```
180+
171181

172182
### flux-local test
173183

@@ -214,6 +224,16 @@ You may also validate `HelmRelease` objects can be templated properly with the `
214224
flag. This will run `kustomize build` then run `helm template` on all the `HelmRelease` objects
215225
found.
216226

227+
Ignore specific paths
228+
229+
Use `--ignore-paths` to omit directories/files from discovery and build when running tests.
230+
Patterns are `.gitignore`-style and may be comma-separated. A trailing slash matches a directory
231+
and all its contents (e.g., `.github/`).
232+
233+
```bash
234+
$ flux-local test --path clusters/prod --ignore-paths ".github/,local/"
235+
```
236+
217237
## GitHub Action
218238

219239
You may use `flux-local` as a github action to verify the health of the cluster on changes
@@ -236,6 +256,19 @@ helm release expansion enabled.
236256
enable-helm: true
237257
```
238258
259+
Ignore paths during builds
260+
261+
You can exclude directories/files from discovery and builds using `.gitignore`-style
262+
patterns via the `ignore-paths` input (comma-separated, relative to `path`). A trailing
263+
slash matches a directory and all its contents (e.g., `.github/`).
264+
265+
```yaml
266+
- uses: allenporter/flux-local/action/[email protected]
267+
with:
268+
path: clusters/prod
269+
ignore-paths: ".github/,scripts/,local/"
270+
```
271+
239272
### diff action
240273

241274
The `diff` action will show you the final diffs of `Kustomization` or `HelmRelease`
@@ -254,6 +287,7 @@ This is an example that diffs a `HelmRelease`:
254287
live-branch: main
255288
path: clusters/prod
256289
resource: helmrelease
290+
ignore-paths: ".github/,scripts/"
257291
- name: PR Comments
258292
uses: mshick/add-pr-comment@v2
259293
if: ${{ steps.diff.outputs.diff != '' }}
@@ -291,6 +325,7 @@ jobs:
291325
live-branch: main
292326
path: ${{ matrix.cluster_path }}
293327
resource: ${{ matrix.resource }}
328+
ignore-paths: ".github/,scripts/"
294329
- name: PR Comments
295330
uses: mshick/add-pr-comment@v2
296331
if: ${{ steps.diff.outputs.diff != '' }}

action/diff/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ inputs:
4040
kustomize-build-flags:
4141
description: Additional flags to pass to kustomize build
4242
default: ""
43+
ignore-paths:
44+
description: Comma-separated .gitignore-style patterns to ignore during build
45+
default: ""
4346
sources:
4447
description: GitRepository or OCIRepository to include with optional source mappings like `flux-system` or `cluster=./kubernetes/`
4548
default: ""
@@ -112,6 +115,7 @@ runs:
112115
--limit-bytes ${{ inputs.limit-bytes }} \
113116
--all-namespaces \
114117
--kustomize-build-flags="${{ inputs.kustomize-build-flags }}" \
118+
--ignore-paths "${{ inputs.ignore-paths }}" \
115119
--sources "${{ inputs.sources }}" \
116120
--output-file diff.patch \
117121
${extra_flags}

action/test/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ inputs:
2222
kustomize-build-flags:
2323
description: Additional flags to pass to kustomize build
2424
default: ""
25+
ignore-paths:
26+
description: Comma-separated .gitignore-style patterns to ignore during build
27+
default: ""
2528
sources:
2629
description: GitRepository or OCIRepository to include with optional source mappings like `flux-system` or `cluster=./kubernetes/`
2730
default: ""
@@ -62,6 +65,7 @@ runs:
6265
--${{ inputs.enable-helm != 'true' && 'no-' || '' }}enable-helm \
6366
--api-versions "${{ inputs.api-versions }}" \
6467
--kustomize-build-flags="${{ inputs.kustomize-build-flags }}" \
68+
--ignore-paths "${{ inputs.ignore-paths }}" \
6569
--sources "${{ inputs.sources }}" \
6670
--path ${{ inputs.path }} \
6771
--verbosity ${{ inputs.debug != 'true' && '0' || '1' }} \

flux_local/git_repo.py

Lines changed: 129 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
)
6464
from .exceptions import InputException
6565
from .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:
407411
class 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)

flux_local/kustomize.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,13 @@ async def _resource_lock(key: str) -> AsyncIterator[None]:
182182
class FluxBuild(Task):
183183
"""A task that issues a flux build command."""
184184

185-
def __init__(self, ks: Kustomization, path: Path) -> None:
185+
def __init__(
186+
self, ks: Kustomization, path: Path, ignore_paths: list[str] | None = None
187+
) -> None:
186188
"""Initialize Build."""
187189
self._ks = ks
188190
self._path = path
191+
self._ignore_paths = list(ignore_paths or [])
189192

190193
async def run(self, stdin: bytes | None = None) -> bytes:
191194
"""Run the task."""
@@ -207,6 +210,11 @@ async def run(self, stdin: bytes | None = None) -> bytes:
207210
"--path",
208211
str(self._path),
209212
]
213+
# Pass through any ignore paths in .gitignore format (comma-separated)
214+
if self._ignore_paths:
215+
combined = ",".join([p for p in self._ignore_paths if p])
216+
if combined:
217+
args.extend(["--ignore-paths", combined])
210218
if self._ks.namespace:
211219
args.extend(
212220
[
@@ -230,9 +238,11 @@ def __str__(self) -> str:
230238
return f"flux build {format_path(self._path)}"
231239

232240

233-
def flux_build(ks: Kustomization, path: Path) -> Kustomize:
241+
def flux_build(
242+
ks: Kustomization, path: Path, ignore_paths: list[str] | None = None
243+
) -> Kustomize:
234244
"""Build cluster artifacts from the specified path."""
235-
return Kustomize(cmds=[FluxBuild(ks, path)])
245+
return Kustomize(cmds=[FluxBuild(ks, path, ignore_paths=ignore_paths)])
236246

237247

238248
def grep(expr: str, path: Path, invert: bool = False) -> Kustomize:

flux_local/tool/selector.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ def add_common_flags(args: ArgumentParser) -> None:
139139
default="",
140140
help="If present, additional flags to pass to `kustomize build`",
141141
)
142+
args.add_argument(
143+
"--ignore-paths",
144+
action="append",
145+
default=[],
146+
help=(
147+
"Comma-separated .gitignore-style patterns to ignore when building. "
148+
"Passed through to `flux build --ignore-paths`. May be set multiple times."
149+
),
150+
)
142151

143152

144153
def add_ks_selector_flags(args: ArgumentParser) -> None:
@@ -162,6 +171,8 @@ def options( # type: ignore[no-untyped-def]
162171
options.skip_kustomize_path_validation = kwargs.get(
163172
"skip_invalid_kustomization_paths", False
164173
)
174+
# Pass through flux ignore paths
175+
options.ignore_paths = kwargs.get("ignore_paths") or []
165176
return options
166177

167178

flux_local/tool/test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ def runtest(self) -> None:
144144
async def async_runtest(self) -> None:
145145
"""Run the Kustomizations test."""
146146
cmd = await kustomize.flux_build(
147-
self.kustomization, Path(self.kustomization.path)
147+
self.kustomization,
148+
Path(self.kustomization.path),
149+
ignore_paths=self.test_config.options.ignore_paths,
148150
).stash()
149151
await cmd.objects()
150152

0 commit comments

Comments
 (0)