Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-flux-local for openSUSE:Factory checked in at 2025-04-25 22:19:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-flux-local (Old) and /work/SRC/openSUSE:Factory/.python-flux-local.new.30101 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-flux-local" Fri Apr 25 22:19:57 2025 rev:4 rq:1272560 version:7.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-flux-local/python-flux-local.changes 2025-01-09 15:11:56.579513167 +0100 +++ /work/SRC/openSUSE:Factory/.python-flux-local.new.30101/python-flux-local.changes 2025-04-25 22:21:17.544536919 +0200 @@ -1,0 +2,74 @@ +Fri Apr 25 06:02:17 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- update to 7.4.0: + * What's Changed + - Update install instructions by @allenporter in #858 + - Add support for targetNamespace by @weisdd in #866 + +------------------------------------------------------------------- +Fri Mar 21 16:49:49 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- update to 7.3.0: + * What's Changed + - Add support for HelmRelease disableSchemaValidation and + disableOpenAPIValidation by @allenporter in #856 + - Remove unnecessary slugify dependency by @filipposc5 in #849 + * Developer updates + - Update tag to avoid drifting repo by @allenporter in #852 + - chore(deps): update dependency ruff to v0.10.0 by @renovate + in #850 + - chore(deps): update pre-commit hook + charliermarsh/ruff-pre-commit to v0.10.0 by @renovate in #851 + - chore(deps): update dependency yamllint to v1.36.0 by + @renovate in #846 + - chore(deps): update pre-commit hook adrienverge/yamllint to + v1.36.0 by @renovate in #847 + - chore(deps): update docker.io/bitnami/kubectl docker tag to + v1.32.3 by @renovate in #848 + +------------------------------------------------------------------- +Thu Mar 6 17:22:44 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- update to 7.2.1: + * What's Changed + - Make OCI chartRef work with optional namespace by @Alexsaphir + in #844 + +------------------------------------------------------------------- +Wed Mar 5 10:49:44 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- update to 7.2.0: + * What's Changed + - Fix OCIRepository support in get cluster by @allenporter in + #843 + - Detect more images from commonly used crds by @buroa in #833 + +------------------------------------------------------------------- +Wed Mar 5 10:46:35 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- update to 7.1.0: + * What's Changed + - fix: remove kubectl from Dockefile by @onedr0p in #822 + - Add get cluster --only-images flag to limit output by + @allenporter in #835 + - Add --output=json for most commands by @allenporter in #836 + - Add label selector in internal code that walks the repo and + matches Kustomization and HelmReleases by @allenporter in + #837 + - Add --label-selector command line flags by @allenporter in + #839 + - Add a flag --skip-kinds to omit kind from the output by + @allenporter in #840 + - Pass OCIRepository chart ref tag to helm --version by + @allenporter in #841 + * Developer updates + - chore(deps): update dependency pip to v25 by @renovate in + #825 + - feat(actions): bump actions/setup-python to v5 by @layertwo + in #827 + - chore(deps): update pre-commit hook psf/black to v25 by + @renovate in #829 + - chore(deps): update dependency black to v25 by @renovate in + #828 + +------------------------------------------------------------------- Old: ---- flux_local-7.0.0.tar.gz New: ---- flux_local-7.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-flux-local.spec ++++++ --- /var/tmp/diff_new_pack.ylD1PJ/_old 2025-04-25 22:21:18.092559950 +0200 +++ /var/tmp/diff_new_pack.ylD1PJ/_new 2025-04-25 22:21:18.096560118 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-flux-local # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2025 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,37 +18,35 @@ %{?sle15_python_module_pythons} Name: python-flux-local -Version: 7.0.0 +Version: 7.4.0 Release: 0 Summary: Set of tools for managing a flux gitops repository License: Apache-2.0 URL: https://github.com/allenporter/flux-local Source: https://files.pythonhosted.org/packages/source/f/flux-local/flux_local-%{version}.tar.gz -BuildRequires: python-rpm-macros BuildRequires: %{python_module pip} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module wheel} +BuildRequires: python-rpm-macros # BuildRequires: %{python_module aiofiles >= 24.1.0} -BuildRequires: %{python_module GitPython >= 3.1.43} +BuildRequires: %{python_module GitPython >= 3.1.44} +BuildRequires: %{python_module PyYAML >= 6.0.2} BuildRequires: %{python_module mashumaro >= 3.15} BuildRequires: %{python_module nest-asyncio >= 1.6.0} -BuildRequires: %{python_module PyYAML >= 6.0.2} -BuildRequires: %{python_module python-slugify >= 8.0.4} # SECTION test requirements -BuildRequires: %{python_module pytest >= 8.3.3} -BuildRequires: %{python_module pytest-asyncio >= 0.25.0} +BuildRequires: %{python_module pytest >= 8.3.5} +BuildRequires: %{python_module pytest-asyncio >= 0.25.3} # /SECTION BuildRequires: fdupes +Requires: python-GitPython >= 3.1.44 +Requires: python-PyYAML >= 6.0.2 Requires: python-aiofiles >= 24.1.0 -Requires: python-GitPython >= 3.1.43 Requires: python-mashumaro >= 3.15 Requires: python-nest-asyncio >= 1.6.0 -Requires: python-python-slugify >= 8.0.4 -Requires: python-PyYAML >= 6.0.2 # Note: flux-local provides repo testing using pytest -Requires: python-pytest >= 8.3.3 -Requires: python-pytest-asyncio >= 0.25.0 +Requires: python-pytest >= 8.3.5 +Requires: python-pytest-asyncio >= 0.25.3 Requires(post): update-alternatives Requires(postun): update-alternatives BuildArch: noarch ++++++ flux_local-7.0.0.tar.gz -> flux_local-7.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/PKG-INFO new/flux_local-7.4.0/PKG-INFO --- old/flux_local-7.0.0/PKG-INFO 2024-12-31 20:47:21.301954000 +0100 +++ new/flux_local-7.4.0/PKG-INFO 2025-04-24 06:04:31.768139100 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: flux-local -Version: 7.0.0 +Version: 7.4.0 Summary: flux-local is a python library and set of tools for managing a flux gitops repository, with validation steps to help improve quality of commits, PRs, and general local testing. Home-page: https://github.com/allenporter/flux-local Author: Allen Porter @@ -12,12 +12,12 @@ License-File: LICENSE Requires-Dist: aiofiles>=22.1.0 Requires-Dist: nest_asyncio>=1.5.6 -Requires-Dist: python-slugify>=8.0.0 Requires-Dist: GitPython>=3.1.30 Requires-Dist: PyYAML>=6.0 Requires-Dist: mashumaro>=3.12 Requires-Dist: pytest>=7.2.1 Requires-Dist: pytest-asyncio>=0.20.3 +Dynamic: license-file flux-local is a set of tools and libraries for managing a local flux gitops repository focused on validation steps to help improve quality of commits, PRs, and general local testing. @@ -36,10 +36,13 @@ ## flux-local CLI -The CLI is written in python and packaged as part of the `flux-local` python library, which can be installed using pip: +The CLI is written in python and packaged as part of the `flux-local` python library, which can be installed using pip and uv. If you have not yet embraced python virtual environments, now might be +the right time to do so to avoid clobbering the packages in your system. ```bash -$ pip3 install flux-local +$ uv venv +$ source .venv/bin/activate +$ uv pip install flux-local ``` ### flux-local get diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/README.md new/flux_local-7.4.0/README.md --- old/flux_local-7.0.0/README.md 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/README.md 2025-04-24 06:04:26.000000000 +0200 @@ -15,10 +15,13 @@ ## flux-local CLI -The CLI is written in python and packaged as part of the `flux-local` python library, which can be installed using pip: +The CLI is written in python and packaged as part of the `flux-local` python library, which can be installed using pip and uv. If you have not yet embraced python virtual environments, now might be +the right time to do so to avoid clobbering the packages in your system. ```bash -$ pip3 install flux-local +$ uv venv +$ source .venv/bin/activate +$ uv pip install flux-local ``` ### flux-local get diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/git_repo.py new/flux_local-7.4.0/flux_local/git_repo.py --- old/flux_local-7.0.0/flux_local/git_repo.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/git_repo.py 2025-04-24 06:04:26.000000000 +0200 @@ -203,10 +203,7 @@ func: Callable[ [ Path, - Kustomization - | HelmRelease - | HelmRepository - | OCIRepository, + Kustomization | HelmRelease | HelmRepository | OCIRepository, kustomize.Kustomize | None, ], Awaitable[None], @@ -255,12 +252,18 @@ namespace: str | None = None """Resources returned will be from this namespace.""" + label_selector: dict[str, str] | None = None + """Resources returned must have these labels.""" + skip_crds: bool = True """If false, CRDs may be processed, depending on the resource type.""" skip_secrets: bool = True """If false, Secrets may be processed, depending on the resource type.""" + skip_kinds: list[str] | None = None + """A list of potential CRDs to skip when emitting objects.""" + visitor: ResourceVisitor | None = None """Visitor for the specified object type that can be used for building.""" @@ -274,12 +277,7 @@ """A predicate that selects Kustomization objects.""" def predicate( - obj: ( - Kustomization - | HelmRelease - | HelmRepository - | OCIRepository - ), + obj: Kustomization | HelmRelease | HelmRepository | OCIRepository, ) -> bool: if not self.enabled: return False @@ -287,6 +285,15 @@ return False if self.namespace and obj.namespace != self.namespace: return False + if self.label_selector and isinstance(obj, (Kustomization, HelmRelease)): + obj_labels = obj.labels or {} + for name, value in self.label_selector.items(): + _LOGGER.debug("Checking %s=%s", name, value) + if ( + obj_value := obj_labels.get(name) + ) is None or obj_value != value: + _LOGGER.debug("mismatch v=%s", obj_value) + return False return True return predicate @@ -596,6 +603,8 @@ skips.append(CRD_KIND) if kustomization_selector.skip_secrets: skips.append(SECRET_KIND) + if kustomization_selector.skip_kinds: + skips.extend(kustomization_selector.skip_kinds) cmd = cmd.skip_resources(skips) try: cmd = await cmd.stash() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/helm.py new/flux_local-7.4.0/flux_local/helm.py --- old/flux_local-7.0.0/flux_local/helm.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/helm.py 2025-04-24 06:04:26.000000000 +0200 @@ -137,6 +137,9 @@ skip_secrets: bool = False """Don't emit secrets in the output.""" + skip_kinds: list[str] | None = None + """Omit these kinds in the output.""" + kube_version: str | None = None """Value of the helm --kube-version flag.""" @@ -173,6 +176,8 @@ skips.append(CRD_KIND) if self.skip_secrets: skips.append(SECRET_KIND) + if self.skip_kinds: + skips.extend(self.skip_kinds) return skips @@ -257,10 +262,14 @@ release.name, _chart_name(release, repo), "--namespace", - release.namespace, + release.release_namespace, ] args.extend(self._flags) args.extend(options.template_args) + if release.disable_openapi_validation: + args.append("--disable-openapi-validation") + if release.disable_schema_validation: + args.append("--skip-schema-validation") if release.chart.version: args.extend( [ @@ -268,6 +277,13 @@ release.chart.version, ] ) + elif isinstance(repo, OCIRepository) and repo.ref_tag: + args.extend( + [ + "--version", + repo.ref_tag, + ] + ) if release.values: values_path = self._tmp_dir / f"{release.release_name}-values.yaml" async with aiofiles.open(values_path, mode="w") as values_file: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/image.py new/flux_local-7.4.0/flux_local/image.py --- old/flux_local-7.0.0/flux_local/image.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/image.py 2025-04-24 06:04:26.000000000 +0200 @@ -18,22 +18,38 @@ "CronJob", "Job", "ReplicationController", + "EMQX", # apps.emqx.io/v2beta1 + "Cluster", # postgresql.cnpg.io/v1 + "CephCluster", # ceph.rook.io/v1 + "Alertmanager", # monitoring.coreos.com/v1 + "Prometheus", # monitoring.coreos.com/v1 + "AutoscalingRunnerSet", # actions.github.com/v1alpha1 ] + +# Default image key for most object types. IMAGE_KEY = "image" +# Override the default image key for some object types. +KINDS_IMAGE_KEY = { + "Cluster": "imageName" +} + -def _extract_images(doc: dict[str, Any]) -> set[str]: +def _extract_images(kind: str, doc: dict[str, Any]) -> set[str]: """Extract the image from a Kubernetes object.""" images: set[str] = set({}) + image_key = KINDS_IMAGE_KEY.get(kind) or IMAGE_KEY + for key, value in doc.items(): - if key == IMAGE_KEY: + if key == image_key: images.add(value) elif isinstance(value, dict): - images.update(_extract_images(value)) + images.update(_extract_images(kind, value)) elif isinstance(value, list): for item in value: if isinstance(item, dict): - images.update(_extract_images(item)) + images.update(_extract_images(kind, item)) + return images @@ -56,7 +72,8 @@ Updates the image set with the images found in the document. """ - images = _extract_images(doc) + kind: str = doc["kind"] + images = _extract_images(kind, doc) if not images: return if name in self.images: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/manifest.py new/flux_local-7.4.0/flux_local/manifest.py --- old/flux_local-7.0.0/flux_local/manifest.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/manifest.py 2025-04-24 06:04:26.000000000 +0200 @@ -140,15 +140,12 @@ raise InputException(f"Invalid {cls} missing spec.chartRef.kind: {doc}") if not (name := chart_ref.get("name")): raise InputException(f"Invalid {cls} missing spec.chartRef.name: {doc}") - if not (namespace := chart_ref.get("namespace")): - raise InputException( - f"Invalid {cls} missing spec.chartRef.namespace: {doc}" - ) + return cls( name=name, version=None, repo_name=name, - repo_namespace=namespace, + repo_namespace=chart_ref.get("namespace", default_namespace), repo_kind=kind, ) if not (chart_spec := chart.get("spec")): @@ -218,6 +215,9 @@ chart: HelmChart """A mapping to a specific helm chart for this HelmRelease.""" + target_namespace: str | None = field(metadata={"serialize": "omit"}, default=None) + """The namespace to target when performing the operation.""" + values: Optional[dict[str, Any]] = field( metadata={"serialize": "omit"}, default=None ) @@ -231,6 +231,19 @@ images: list[str] | None = field(default=None) """The list of images referenced in the HelmRelease.""" + labels: dict[str, str] | None = field(metadata={"serialize": "omit"}, default=None) + """A list of labels on the HelmRelease.""" + + disable_schema_validation: bool = field( + metadata={"serialize": "omit"}, default=False + ) + """Prevents Helm from validating the values against the JSON Schema.""" + + disable_openapi_validation: bool = field( + metadata={"serialize": "omit"}, default=False + ) + """Prevents Helm from validating the values against the Kubernetes OpenAPI Schema.""" + @classmethod def parse_doc(cls, doc: dict[str, Any]) -> "HelmRelease": """Parse a HelmRelease from a kubernetes resource object.""" @@ -248,12 +261,26 @@ values_from = [ ValuesReference.from_dict(subdoc) for subdoc in values_from_dict ] + disable_schema_validation = any( + bag.get("disableSchemaValidation") + for key in ("install", "upgrade") + if (bag := spec.get(key)) is not None + ) + disable_openapi_validation = any( + bag.get("disableOpenAPIValidation") + for key in ("install", "upgrade") + if (bag := spec.get(key)) is not None + ) return HelmRelease( name=name, namespace=namespace, + target_namespace=spec.get("targetNamespace"), chart=chart, values=spec.get("values"), values_from=values_from, + labels=metadata.get("labels"), + disable_schema_validation=disable_schema_validation, + disable_openapi_validation=disable_openapi_validation, ) @property @@ -262,6 +289,13 @@ return f"{self.namespace}-{self.name}" @property + def release_namespace(self) -> str: + """Actual namespace where the HelmRelease will be installed to.""" + if self.target_namespace: + return self.target_namespace + return self.namespace + + @property def repo_name(self) -> str: """Identifier for the HelmRepository identified in the HelmChart.""" return f"{self.chart.repo_namespace}-{self.chart.repo_name}" @@ -356,6 +390,9 @@ url: str """The URL to the repository.""" + ref_tag: str | None = None + """The version tag of the repository.""" + @classmethod def parse_doc(cls, doc: dict[str, Any]) -> "OCIRepository": """Parse a HelmRepository from a kubernetes resource.""" @@ -370,10 +407,12 @@ raise InputException(f"Invalid {cls} missing spec: {doc}") if not (url := spec.get("url")): raise InputException(f"Invalid {cls} missing spec.url: {doc}") + ref_tag = spec.get("ref", {}).get("tag") return cls( name=name, namespace=namespace, url=url, + ref_tag=ref_tag, ) @property @@ -542,6 +581,9 @@ depends_on: list[str] | None = field(metadata={"serialize": "omit"}, default=None) """A list of namespaced names that this Kustomization depends on.""" + labels: dict[str, str] | None = field(metadata={"serialize": "omit"}, default=None) + """A list of labels on the Kustomization.""" + @classmethod def parse_doc(cls, doc: dict[str, Any]) -> "Kustomization": """Parse a partial Kustomization from a kubernetes resource.""" @@ -582,6 +624,7 @@ postbuild_substitute=postbuild.get("substitute"), postbuild_substitute_from=substitute_from, depends_on=depends_on, + labels=metadata.get("labels"), ) @property diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/resource_diff.py new/flux_local-7.4.0/flux_local/resource_diff.py --- old/flux_local-7.0.0/flux_local/resource_diff.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/resource_diff.py 2025-04-24 06:04:26.000000000 +0200 @@ -3,7 +3,7 @@ This is used internally, primarily by the diff tool. """ -from collections.abc import Iterable +from collections.abc import Iterable, Callable from dataclasses import asdict import difflib import logging @@ -11,6 +11,7 @@ import tempfile from typing import Generator, Any, AsyncGenerator, TypeVar import yaml +import json from . import command @@ -118,7 +119,40 @@ ) -> Generator[str, None, None]: """Generate diffs between the two output objects.""" - diffs = [] + def diff_func(diffs: list[dict[str, Any]]) -> str: + return yaml.dump( + diffs, sort_keys=False, explicit_start=True, default_style=None + ) + + for result in _perform_function_diff(a, b, n, limit_bytes, diff_func): + yield result + + +def perform_json_diff( + a: ObjectOutput, + b: ObjectOutput, + n: int, + limit_bytes: int, +) -> Generator[str, None, None]: + """Generate diffs between the two output objects.""" + + def diff_func(diffs: list[dict[str, Any]]) -> str: + return json.dumps(diffs, sort_keys=False, indent=4) + + for result in _perform_function_diff(a, b, n, limit_bytes, diff_func): + yield result + + +def _perform_function_diff( + a: ObjectOutput, + b: ObjectOutput, + n: int, + limit_bytes: int, + diff_func: Callable[[list[dict[str, Any]]], str], +) -> Generator[str, None, None]: + """Generate diffs between the two output objects.""" + + diffs: list[dict[str, Any]] = [] for kustomization_key in _unique_keys(a.content, b.content): _LOGGER.debug("Diffing results for %s (n=%d)", kustomization_key, n) a_resources = a.content.get(kustomization_key, {}) @@ -150,7 +184,7 @@ } ) if diffs: - yield yaml.dump(diffs, sort_keys=False, explicit_start=True, default_style=None) + yield diff_func(diffs) def merge_helm_releases( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/tool/build.py new/flux_local-7.4.0/flux_local/tool/build.py --- old/flux_local-7.0.0/flux_local/tool/build.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/tool/build.py 2025-04-24 06:04:26.000000000 +0200 @@ -65,6 +65,7 @@ enable_helm: bool, skip_crds: bool, skip_secrets: bool, + skip_kinds: list[str], output_file: str, **kwargs, # pylint: disable=unused-argument ) -> None: @@ -74,10 +75,11 @@ query.kustomization.namespace = None query.kustomization.skip_crds = skip_crds query.kustomization.skip_secrets = skip_secrets + query.kustomization.skip_kinds = skip_kinds query.helm_release.enabled = enable_helm query.helm_release.namespace = None helm_options = selector.build_helm_options( - skip_crds=skip_crds, skip_secrets=skip_secrets, **kwargs + skip_crds=skip_crds, skip_secrets=skip_secrets, skip_kinds=skip_kinds, **kwargs ) content = ContentOutput() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/tool/diff.py new/flux_local-7.4.0/flux_local/tool/diff.py --- old/flux_local-7.0.0/flux_local/tool/diff.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/tool/diff.py 2025-04-24 06:04:26.000000000 +0200 @@ -20,6 +20,7 @@ perform_external_diff, perform_object_diff, build_helm_dependency_map, + perform_json_diff, ) from . import selector @@ -35,7 +36,7 @@ args.add_argument( "--output", "-o", - choices=["diff", "yaml", "object"], + choices=["diff", "yaml", "object", "json"], default="diff", help="Output format of the command", ) @@ -163,6 +164,10 @@ result = perform_yaml_diff(orig_content, content, unified, limit_bytes) for line in result: print(line, file=file) + elif output == "json": + result = perform_json_diff(orig_content, content, unified, limit_bytes) + for line in result: + print(line, file=file) elif external_diff := os.environ.get("DIFF"): async for line in perform_external_diff( shlex.split(external_diff), orig_content, content, limit_bytes @@ -253,7 +258,9 @@ # depends on in the kustomization has a diff. This avoids needing to # template every possible helm chart when nothing as changed. dependency_map = build_helm_dependency_map(orig_helm_visitor, helm_visitor) - diff_resource_keys = get_helm_release_diff_keys(orig_content, content, dependency_map) + diff_resource_keys = get_helm_release_diff_keys( + orig_content, content, dependency_map + ) diff_names = { resource_key.namespaced_name for resource_key in diff_resource_keys } @@ -286,6 +293,11 @@ orig_helm_content, helm_content, unified, limit_bytes ): print(line, file=file) + elif output == "json": + for line in perform_json_diff( + orig_helm_content, helm_content, unified, limit_bytes + ): + print(line, file=file) elif external_diff := os.environ.get("DIFF"): async for line in perform_external_diff( shlex.split(external_diff), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/tool/format.py new/flux_local-7.4.0/flux_local/tool/format.py --- old/flux_local-7.0.0/flux_local/tool/format.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/tool/format.py 2025-04-24 06:04:26.000000000 +0200 @@ -1,10 +1,12 @@ """Library for formatting output.""" +from abc import ABC, abstractmethod from typing import Generator, Any import sys from typing import TextIO import yaml +import json PADDING = 4 @@ -56,18 +58,57 @@ print(result, file=file) -class YamlFormatter: +class StructFormatter(ABC): + """A formatter that prints objects.""" + + @abstractmethod + def format(self, data: Any) -> Generator[str, None, None]: + """Format the data objects.""" + + @abstractmethod + def print(self, data: Any, file: TextIO = sys.stdout) -> None: + """Print the data objects.""" + + +class YamlFormatter(StructFormatter): """A formatter that prints yaml output.""" - def format(self, data: list[dict[str, Any]]) -> Generator[str, None, None]: + def format(self, data: Any) -> Generator[str, None, None]: """Format the data objects.""" for line in yaml.dump_all(data, sort_keys=False, explicit_start=True).split( "\n" ): yield line - def print(self, data: list[dict[str, Any]], file: TextIO = sys.stdout) -> None: + def print(self, data: Any, file: TextIO = sys.stdout) -> None: """Format the data objects.""" print( yaml.dump_all(data, sort_keys=False, explicit_start=True), end="", file=file ) + + +class YamlListFormatter(StructFormatter): + """A formatter that prints yaml output for a list instead of a document.""" + + def format(self, data: Any) -> Generator[str, None, None]: + """Format the data objects.""" + content = yaml.dump(data, sort_keys=False, explicit_start=True) + for line in content.split("\n"): + yield line + + def print(self, data: Any, file: TextIO = sys.stdout) -> None: + """Format the data objects.""" + print(yaml.dump(data, sort_keys=False, explicit_start=True), end="", file=file) + + +class JsonFormatter(StructFormatter): + """A formatter that prints json output.""" + + def format(self, data: Any) -> Generator[str, None, None]: + """Format the data objects.""" + for line in json.dumps(data, indent=4, sort_keys=False).split("\n"): + yield line + + def print(self, data: Any, file: TextIO = sys.stdout) -> None: + """Format the data objects.""" + json.dump(data, sort_keys=False, indent=4, fp=file) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/tool/get.py new/flux_local-7.4.0/flux_local/tool/get.py --- old/flux_local-7.0.0/flux_local/tool/get.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/tool/get.py 2025-04-24 06:04:26.000000000 +0200 @@ -14,7 +14,13 @@ from flux_local import git_repo, image, helm from flux_local.visitor import HelmVisitor, ImageOutput -from .format import PrintFormatter, YamlFormatter +from .format import ( + PrintFormatter, + YamlFormatter, + YamlListFormatter, + JsonFormatter, + StructFormatter, +) from . import selector @@ -168,9 +174,16 @@ help="Output container images when traversing the cluster", ) args.add_argument( + "--only-images", + type=str, + default=False, + action=BooleanOptionalAction, + help="Output only container images when traversing the cluster", + ) + args.add_argument( "--output", "-o", - choices=["diff", "yaml"], + choices=["diff", "yaml", "json"], default="diff", help="Output format of the command", ) @@ -188,33 +201,43 @@ output: str, output_file: str, enable_images: bool, + only_images: bool, **kwargs, # pylint: disable=unused-argument ) -> None: """Async Action implementation.""" + if output not in {"yaml", "json"}: + if enable_images: + print( + "Flag --enable-images only works with --output yaml or json", + file=sys.stderr, + ) + return + if only_images and not enable_images: + print( + "Flag --only-images only works with --enable-images", + file=sys.stderr, + ) + return + query = selector.build_cluster_selector(**kwargs) - query.helm_release.enabled = output == "yaml" + query.helm_release.enabled = output in {"yaml", "json"} image_visitor: image.ImageVisitor | None = None helm_content: ImageOutput | None = None if enable_images: - if output != "yaml": - print( - "Flag --enable-images only works with --output yaml", - file=sys.stderr, - ) - return image_visitor = image.ImageVisitor() query.doc_visitor = image_visitor.repo_visitor() helm_content = ImageOutput() helm_visitor = HelmVisitor() query.helm_repo.visitor = helm_visitor.repo_visitor() + query.oci_repo.visitor = helm_visitor.repo_visitor() query.helm_release.visitor = helm_visitor.release_visitor() manifest = await git_repo.build_manifest( selector=query, options=selector.options(**kwargs) ) - if output == "yaml": + if output == "yaml" or output == "json": if image_visitor: image_visitor.update_manifest(manifest) if helm_content: @@ -226,8 +249,35 @@ ) helm_content.update_manifest(manifest) + output_content: Any + formatter: StructFormatter + if only_images: + output_content = sorted( + list( + { + *[ + image + for cluster in manifest.clusters + for hr in cluster.helm_releases + for image in hr.images or () + ], + *[ + image + for cluster in manifest.clusters + for ks in cluster.kustomizations + for image in ks.images or () + ], + } + ) + ) + formatter = YamlListFormatter() if output == "yaml" else JsonFormatter() + else: + output_content = manifest.compact_dict() + if output == "yaml": # Yaml is printing multiple docs + output_content = [output_content] + formatter = YamlFormatter() if output == "yaml" else JsonFormatter() with open(output_file, "w") as file: - YamlFormatter().print([manifest.compact_dict()], file=file) + formatter.print(output_content, file=file) return cols = ["path", "kustomizations"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local/tool/selector.py new/flux_local-7.4.0/flux_local/tool/selector.py --- old/flux_local-7.0.0/flux_local/tool/selector.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/flux_local/tool/selector.py 2025-04-24 06:04:26.000000000 +0200 @@ -44,6 +44,28 @@ setattr(namespace, self.dest, result) +class SelectorAppendAction(Action): + """Append a key=value pair to the argument dict.""" + + def __call__( + self, + parser: ArgumentParser, + namespace: Namespace, + values: Any, + option_string: str | None = None, + ) -> None: + values = values.split(",") + if not values[0]: + return + result = getattr(namespace, self.dest) or {} + for value in values: + if "=" not in value: + raise ValueError(f"Expected key=value format but got '{value}'") + k, v = value.split("=") + result[k] = v + setattr(namespace, self.dest, result) + + def add_selector_flags(args: ArgumentParser) -> None: """Add common selector flags to the arguments object.""" args.add_argument( @@ -75,6 +97,12 @@ default=DEFAULT_NAMESPACE, help="If present, the namespace scope for this request", ) + args.add_argument( + "--label-selector", + "-l", + action=SelectorAppendAction, + help="Filter objects by label selector by name=value", + ) add_common_flags(args) @@ -95,6 +123,11 @@ help="When true do not include Secrets to reduce output size and randomness", ) args.add_argument( + "--skip-kinds", + type=lambda x: x.split(","), + help="A comma separated list of CRDs to omit from the output.", + ) + args.add_argument( "--kustomize-build-flags", type=str, default="", @@ -135,8 +168,10 @@ selector.kustomization.namespace = kwargs["namespace"] if kwargs["all_namespaces"]: selector.kustomization.namespace = None + selector.kustomization.label_selector = kwargs["label_selector"] selector.kustomization.skip_crds = kwargs["skip_crds"] selector.kustomization.skip_secrets = kwargs["skip_secrets"] + selector.kustomization.skip_kinds = kwargs["skip_kinds"] return selector @@ -165,8 +200,10 @@ selector.helm_release.namespace = kwargs["namespace"] if kwargs["all_namespaces"]: selector.helm_release.namespace = None + selector.helm_release.label_selector = kwargs["label_selector"] selector.helm_release.skip_crds = kwargs["skip_crds"] selector.helm_release.skip_secrets = kwargs["skip_secrets"] + selector.helm_release.skip_kinds = kwargs["skip_kinds"] selector.kustomization.name = None selector.kustomization.namespace = None return selector @@ -198,6 +235,7 @@ return helm.Options( skip_crds=kwargs["skip_crds"], skip_secrets=kwargs["skip_secrets"], + skip_kinds=kwargs["skip_kinds"], kube_version=kwargs.get("kube_version"), api_versions=kwargs.get("api_versions"), registry_config=kwargs.get("registry_config"), @@ -222,8 +260,10 @@ if kwargs.get("all_namespaces"): selector.cluster.namespace = None selector.kustomization.namespace = None + selector.kustomization.label_selector = kwargs["label_selector"] selector.kustomization.skip_crds = kwargs["skip_crds"] selector.kustomization.skip_secrets = kwargs["skip_secrets"] + selector.kustomization.skip_kinds = kwargs["skip_kinds"] return selector diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local.egg-info/PKG-INFO new/flux_local-7.4.0/flux_local.egg-info/PKG-INFO --- old/flux_local-7.0.0/flux_local.egg-info/PKG-INFO 2024-12-31 20:47:21.000000000 +0100 +++ new/flux_local-7.4.0/flux_local.egg-info/PKG-INFO 2025-04-24 06:04:31.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: flux-local -Version: 7.0.0 +Version: 7.4.0 Summary: flux-local is a python library and set of tools for managing a flux gitops repository, with validation steps to help improve quality of commits, PRs, and general local testing. Home-page: https://github.com/allenporter/flux-local Author: Allen Porter @@ -12,12 +12,12 @@ License-File: LICENSE Requires-Dist: aiofiles>=22.1.0 Requires-Dist: nest_asyncio>=1.5.6 -Requires-Dist: python-slugify>=8.0.0 Requires-Dist: GitPython>=3.1.30 Requires-Dist: PyYAML>=6.0 Requires-Dist: mashumaro>=3.12 Requires-Dist: pytest>=7.2.1 Requires-Dist: pytest-asyncio>=0.20.3 +Dynamic: license-file flux-local is a set of tools and libraries for managing a local flux gitops repository focused on validation steps to help improve quality of commits, PRs, and general local testing. @@ -36,10 +36,13 @@ ## flux-local CLI -The CLI is written in python and packaged as part of the `flux-local` python library, which can be installed using pip: +The CLI is written in python and packaged as part of the `flux-local` python library, which can be installed using pip and uv. If you have not yet embraced python virtual environments, now might be +the right time to do so to avoid clobbering the packages in your system. ```bash -$ pip3 install flux-local +$ uv venv +$ source .venv/bin/activate +$ uv pip install flux-local ``` ### flux-local get diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/flux_local.egg-info/requires.txt new/flux_local-7.4.0/flux_local.egg-info/requires.txt --- old/flux_local-7.0.0/flux_local.egg-info/requires.txt 2024-12-31 20:47:21.000000000 +0100 +++ new/flux_local-7.4.0/flux_local.egg-info/requires.txt 2025-04-24 06:04:31.000000000 +0200 @@ -1,6 +1,5 @@ aiofiles>=22.1.0 nest_asyncio>=1.5.6 -python-slugify>=8.0.0 GitPython>=3.1.30 PyYAML>=6.0 mashumaro>=3.12 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/setup.cfg new/flux_local-7.4.0/setup.cfg --- old/flux_local-7.0.0/setup.cfg 2024-12-31 20:47:21.301954000 +0100 +++ new/flux_local-7.4.0/setup.cfg 2025-04-24 06:04:31.769139300 +0200 @@ -1,6 +1,6 @@ [metadata] name = flux-local -version = 7.0.0 +version = 7.4.0 description = flux-local is a python library and set of tools for managing a flux gitops repository, with validation steps to help improve quality of commits, PRs, and general local testing. long_description = file: README.md long_description_content_type = text/markdown @@ -19,7 +19,6 @@ install_requires = aiofiles>=22.1.0 nest_asyncio>=1.5.6 - python-slugify>=8.0.0 GitPython>=3.1.30 PyYAML>=6.0 mashumaro>=3.12 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/tests/test_git_repo.py new/flux_local-7.4.0/tests/test_git_repo.py --- old/flux_local-7.0.0/tests/test_git_repo.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/tests/test_git_repo.py 2025-04-24 06:04:26.000000000 +0200 @@ -282,3 +282,80 @@ ) selector = PathSelector(TESTDATA_FULL_PATH) assert adjust_ks_path(ks, selector) == Path(expected_path) + + +async def test_kustomization_label_selector() -> None: + """Tests for building the manifest.""" + + query = ResourceSelector() + query.path.path = TESTDATA + + async def get_ks() -> list[str]: + manifest = await build_manifest(selector=query) + return [ + ks.namespaced_name + for cluster in manifest.clusters + for ks in cluster.kustomizations + ] + + assert await get_ks() == [ + "flux-system/apps", + "flux-system/flux-system", + "flux-system/infra-configs", + "flux-system/infra-controllers", + ] + + query.kustomization.label_selector = {"app.kubernetes.io/name": "apps"} + assert await get_ks() == [ + "flux-system/apps", + ] + + query.kustomization.label_selector = {"app.kubernetes.io/name": "podinfo"} + assert await get_ks() == [] + + # Match on multiple fields + query.kustomization.label_selector = { + "app.kubernetes.io/name": "apps", + "app.kubernetes.io/instance": "apps", + } + assert await get_ks() == [ + "flux-system/apps", + ] + + # Mismatch on one field + query.kustomization.label_selector = { + "app.kubernetes.io/name": "apps", + "app.kubernetes.io/instance": "flux-system", + } + assert await get_ks() == [] + + +async def test_helmrelease_label_selector() -> None: + """Tests for building the manifest.""" + + query = ResourceSelector() + query.path.path = TESTDATA + + async def get_hr() -> list[str]: + manifest = await build_manifest(selector=query) + return [ + hr.namespaced_name + for cluster in manifest.clusters + for hr in cluster.helm_releases + ] + + assert await get_hr() == [ + "podinfo/podinfo", + "metallb/metallb", + "flux-system/weave-gitops", + ] + + query.helm_release.label_selector = {"app.kubernetes.io/name": "podinfo"} + assert await get_hr() == [ + "podinfo/podinfo", + ] + + query.helm_release.label_selector = { + "app.kubernetes.io/name": "kubernetes-dashboard" + } + assert await get_hr() == [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/tests/test_helm.py new/flux_local-7.4.0/tests/test_helm.py --- old/flux_local-7.0.0/tests/test_helm.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/tests/test_helm.py 2025-04-24 06:04:26.000000000 +0200 @@ -93,11 +93,24 @@ await helm.update() assert len(helm_releases) == 2 + + # metallb, no targetNamespace overrides release = helm_releases[0] obj = await helm.template(HelmRelease.parse_doc(release)) docs = await obj.grep("kind=ServiceAccount").objects() names = [doc.get("metadata", {}).get("name") for doc in docs] + namespaces = [doc.get("metadata", {}).get("namespace") for doc in docs] assert names == ["metallb-controller", "metallb-speaker"] + assert namespaces == ["metallb", "metallb"] + + # weave-gitops, with targetNamespace overrides + release = helm_releases[1] + obj = await helm.template(HelmRelease.parse_doc(release)) + docs = await obj.grep("kind=ServiceAccount").objects() + names = [doc.get("metadata", {}).get("name") for doc in docs] + namespaces = [doc.get("metadata", {}).get("namespace") for doc in docs] + assert names == ["weave-gitops"] + assert namespaces == ["weave"] @pytest.mark.parametrize( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/tests/test_image.py new/flux_local-7.4.0/tests/test_image.py --- old/flux_local-7.0.0/tests/test_image.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/tests/test_image.py 2025-04-24 06:04:26.000000000 +0200 @@ -24,6 +24,15 @@ CWD / "tests/testdata/cluster7", {}, ), + ( + CWD / "tests/testdata/cluster", + { + "flux-system/infra-configs": { + "ceph/ceph:v16.2.6", + "ghcr.io/cloudnative-pg/postgis:17-3.4", + }, + }, + ) ], ) async def test_image_visitor( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/tests/test_resource_diff.py new/flux_local-7.4.0/tests/test_resource_diff.py --- old/flux_local-7.0.0/tests/test_resource_diff.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/tests/test_resource_diff.py 2025-04-24 06:04:26.000000000 +0200 @@ -7,6 +7,7 @@ from collections.abc import Generator import yaml +from syrupy.assertion import SnapshotAssertion from flux_local import git_repo from flux_local.resource_diff import ( @@ -14,6 +15,8 @@ merge_helm_releases, merge_named_resources, build_helm_dependency_map, + perform_json_diff, + perform_yaml_diff, ) from flux_local.visitor import ObjectOutput, HelmVisitor, ResourceKey from flux_local.manifest import HelmRelease, HelmChart, NamedResource @@ -198,3 +201,49 @@ NamedResource(name="secret", namespace="podinfo", kind="Secret"), NamedResource(name="podinfo", namespace="flux-system", kind="HelmRepository"), ] + + +async def test_perform_yaml_diff(snapshot: SnapshotAssertion) -> None: + """Test perform_yaml_diff where an input HelmRepository change causes a diff.""" + + content, helm_visitor = await visit_helm_content(TESTDATA_PATH) + with mirror_worktree(TESTDATA_PATH) as new_cluster_path: + + # Generate a diff by changing a value in the HelmRelease + podinfo_path = new_cluster_path / "apps/podinfo.yaml" + podinfo_doc = list( + yaml.load_all(podinfo_path.read_text(), Loader=yaml.SafeLoader) + ) + assert len(podinfo_doc) == 2 + assert podinfo_doc[0]["kind"] == "HelmRepository" + assert podinfo_doc[0]["spec"]["url"] == "oci://ghcr.io/stefanprodan/charts" + podinfo_doc[0]["spec"]["url"] = "oci://ghcr.io/stefanprodan/charts2" + podinfo_path.write_text(yaml.dump_all(podinfo_doc)) + + content_new, helm_visitor_new = await visit_helm_content(new_cluster_path) + + diff = "\n".join(list(perform_yaml_diff(content, content_new, 5, 50000))) + assert diff == snapshot + + +async def test_perform_json_diff(snapshot: SnapshotAssertion) -> None: + """Test perform_yaml_diff where an input HelmRepository change causes a diff.""" + + content, helm_visitor = await visit_helm_content(TESTDATA_PATH) + with mirror_worktree(TESTDATA_PATH) as new_cluster_path: + + # Generate a diff by changing a value in the HelmRelease + podinfo_path = new_cluster_path / "apps/podinfo.yaml" + podinfo_doc = list( + yaml.load_all(podinfo_path.read_text(), Loader=yaml.SafeLoader) + ) + assert len(podinfo_doc) == 2 + assert podinfo_doc[0]["kind"] == "HelmRepository" + assert podinfo_doc[0]["spec"]["url"] == "oci://ghcr.io/stefanprodan/charts" + podinfo_doc[0]["spec"]["url"] = "oci://ghcr.io/stefanprodan/charts2" + podinfo_path.write_text(yaml.dump_all(podinfo_doc)) + + content_new, helm_visitor_new = await visit_helm_content(new_cluster_path) + + diff = "\n".join(list(perform_json_diff(content, content_new, 5, 50000))) + assert diff == snapshot diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/tests/tool/test_get_cluster.py new/flux_local-7.4.0/tests/tool/test_get_cluster.py --- old/flux_local-7.0.0/tests/tool/test_get_cluster.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/tests/tool/test_get_cluster.py 2025-04-24 06:04:26.000000000 +0200 @@ -30,7 +30,47 @@ (["--path", "tests/testdata/cluster", "-o", "yaml", "--enable-images"]), (["--path", "tests/testdata/cluster8", "-o", "yaml"]), (["--path", "tests/testdata/cluster8", "-o", "yaml", "--enable-images"]), - (["--path", "tests/testdata/cluster8", "-o", "yaml", "--enable-images", "--no-skip-secrets"]), + ( + [ + "--path", + "tests/testdata/cluster8", + "-o", + "yaml", + "--enable-images", + "--no-skip-secrets", + ] + ), + ( + [ + "--path", + "tests/testdata/cluster8", + "-o", + "yaml", + "--enable-images", + "--only-images", + ] + ), + ( + [ + "--path", + "tests/testdata/cluster9/clusters/dev", + "--output", + "yaml", + "--enable-images", + ] + ), + (["--path", "tests/testdata/cluster", "-o", "json"]), + (["--path", "tests/testdata/cluster", "-o", "json", "--enable-images"]), + ( + [ + "--path", + "tests/testdata/cluster8", + "-o", + "json", + "--enable-images", + "--only-images", + ] + ), ], ids=[ "cluster", @@ -47,6 +87,11 @@ "yaml-cluster8-no-images", "yaml-cluster8-images", "yaml-cluster8-images-allow-secrets", + "yaml-cluster8-only-images", + "yaml-cluster9-only-images-with-oci", + "json-cluster", + "json-cluster-images", + "json-cluster-only-images", ], ) async def test_get_cluster(args: list[str], snapshot: SnapshotAssertion) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flux_local-7.0.0/tests/tool/test_get_hr.py new/flux_local-7.4.0/tests/tool/test_get_hr.py --- old/flux_local-7.0.0/tests/tool/test_get_hr.py 2024-12-31 20:47:07.000000000 +0100 +++ new/flux_local-7.4.0/tests/tool/test_get_hr.py 2025-04-24 06:04:26.000000000 +0200 @@ -29,6 +29,24 @@ (["metallb", "-A", "--path", "tests/testdata/cluster"]), (["-n", "metallb", "--path", "tests/testdata/cluster"]), (["-A", "--path", "tests/testdata/cluster9/clusters/dev"]), + ( + [ + "-A", + "--path", + "tests/testdata/cluster", + "-l", + "app.kubernetes.io/name=podinfo", + ] + ), + ( + [ + "-A", + "--path", + "tests/testdata/cluster", + "-l", + "app.kubernetes.io/name=kubernetes-dashboard", + ] + ), ], ids=[ "cluster", @@ -42,6 +60,8 @@ "all_namespace", "name", "cluster9", + "label-selector-match", + "label-selector-no-match", ], ) async def test_get_hr(args: list[str], snapshot: SnapshotAssertion) -> None: