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 2026-01-13 21:30:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-flux-local (Old)
and /work/SRC/openSUSE:Factory/.python-flux-local.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-flux-local"
Tue Jan 13 21:30:41 2026 rev:16 rq:1326908 version:8.1.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-flux-local/python-flux-local.changes
2025-11-17 12:25:23.870879494 +0100
+++
/work/SRC/openSUSE:Factory/.python-flux-local.new.1928/python-flux-local.changes
2026-01-13 21:32:36.147116106 +0100
@@ -1,0 +2,35 @@
+Tue Jan 13 06:43:49 UTC 2026 - Johannes Kastl
<[email protected]>
+
+- update to 8.1.0:
+ * Apply cruft updates by @allenporter in #1046
+ * Update kubectl version in Dockerfile by @allenporter in #1047
+ * feat(tests): add new test cases for build hr-new command by
+ @allenporter in #1048
+ * chore(deps): update fluxcd/flux2 action to v2.7.4 by
+ @renovate[bot] in #1051
+ * Fix races in new controller flows by @allenporter in #1052
+ * Add get ks-new and hr-new for the new API by @allenporter in
+ #1053
+ * New updates detected with Cruft by @github-actions[bot] in
+ #1055
+ * Small fixes by @mouchar in #1065
+ * chore: Update snapshots for latest helm releases by
+ @allenporter in #1067
+ * chore(deps): update dependency coverage to v7.13.0 by
+ @renovate[bot] in #1057
+ * chore(deps): update codecov/codecov-action action to v5.5.2 by
+ @renovate[bot] in #1058
+ * chore(deps): update pre-commit hook astral-sh/ruff-pre-commit
+ to v0.14.10 by @renovate[bot] in #1062
+ * chore(deps): update dependency ruff to v0.14.10 - autoclosed by
+ @renovate[bot] in #1061
+ * chore(deps): update registry.k8s.io/kubectl docker tag to
+ v1.35.0 by @renovate[bot] in #1059
+ * chore(deps): update github artifact actions (major) by
+ @renovate[bot] in #1063
+ * chore(deps): update dependency mypy to v1.19.1 by
+ @renovate[bot] in #1064
+ * feat(gha): add skip-invalid-kustomization-paths to github
+ action by @layertwo in #1069
+
+-------------------------------------------------------------------
Old:
----
flux_local-8.0.1.tar.gz
New:
----
flux_local-8.1.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-flux-local.spec ++++++
--- /var/tmp/diff_new_pack.M3lCRg/_old 2026-01-13 21:32:37.083155400 +0100
+++ /var/tmp/diff_new_pack.M3lCRg/_new 2026-01-13 21:32:37.087155568 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-flux-local
#
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -21,7 +21,7 @@
%{?sle15_python_module_pythons}
Name: python-flux-local
-Version: 8.0.1
+Version: 8.1.0
Release: 0
Summary: Set of tools for managing a flux gitops repository
License: Apache-2.0
++++++ flux_local-8.0.1.tar.gz -> flux_local-8.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/PKG-INFO
new/flux_local-8.1.0/PKG-INFO
--- old/flux_local-8.0.1/PKG-INFO 2025-11-17 06:09:25.379094800 +0100
+++ new/flux_local-8.1.0/PKG-INFO 2025-12-24 21:23:53.666897500 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: flux_local
-Version: 8.0.1
+Version: 8.1.0
Summary: 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.
Author-email: Allen Porter <[email protected]>
License-Expression: Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/helm.py
new/flux_local-8.1.0/flux_local/helm.py
--- old/flux_local-8.0.1/flux_local/helm.py 2025-11-17 06:09:18.000000000
+0100
+++ new/flux_local-8.1.0/flux_local/helm.py 2025-12-24 21:23:47.000000000
+0100
@@ -381,7 +381,9 @@
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:
- await values_file.write(yaml.dump(release.values,
sort_keys=False))
+ await values_file.write(
+ yaml.dump(release.values, sort_keys=False,
default_style='"')
+ )
args.extend(["--values", str(values_path)])
cmd = Kustomize([command.Command(args, exc=HelmException)])
if options.skip_resources:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/flux_local-8.0.1/flux_local/helm_controller/artifact.py
new/flux_local-8.1.0/flux_local/helm_controller/artifact.py
--- old/flux_local-8.0.1/flux_local/helm_controller/artifact.py 2025-11-17
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/helm_controller/artifact.py 2025-12-24
21:23:47.000000000 +0100
@@ -8,8 +8,6 @@
from dataclasses import dataclass
from typing import Any
-import yaml
-
from flux_local.store.artifact import Artifact
@@ -27,10 +25,5 @@
values: dict[str, Any]
"""Resolved values used for rendering the HelmRelease."""
- objects: list[dict[str, Any]]
+ manifests: list[dict[str, Any]]
"""The rendered Kubernetes objects, output of templating the Helm Chart."""
-
- @property
- def manifests(self) -> list[str]:
- """List of rendered Kubernetes manifests as YAML strings."""
- return [yaml.dump(obj, sort_keys=False) for obj in self.objects]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/flux_local-8.0.1/flux_local/helm_controller/controller.py
new/flux_local-8.1.0/flux_local/helm_controller/controller.py
--- old/flux_local-8.0.1/flux_local/helm_controller/controller.py
2025-11-17 06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/helm_controller/controller.py
2025-12-24 21:23:47.000000000 +0100
@@ -201,7 +201,7 @@
# Store the result
artifact = HelmReleaseArtifact(
chart_name=helm_release.chart.chart_name,
- objects=objects,
+ manifests=objects,
values=helm_release.values or {},
)
self.store.set_artifact(resource_id, artifact)
@@ -213,6 +213,13 @@
dependencies = set()
if helm_release.values_from:
for ref in helm_release.values_from:
+ if ref.optional:
+ _LOGGER.debug(
+ "Skipping optional ValuesFrom reference %s for %s",
+ ref.name,
+ helm_release.namespaced_name,
+ )
+ continue
if ref.kind == CONFIG_MAP_KIND:
dependencies.add(
NamedResource(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/flux_local-8.0.1/flux_local/orchestrator/orchestrator.py
new/flux_local-8.1.0/flux_local/orchestrator/orchestrator.py
--- old/flux_local-8.0.1/flux_local/orchestrator/orchestrator.py
2025-11-17 06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/orchestrator/orchestrator.py
2025-12-24 21:23:47.000000000 +0100
@@ -227,7 +227,7 @@
)
)
- async def bootstrap(self, options: BootstrapOptions) -> bool:
+ async def bootstrap(self, options: BootstrapOptions) -> None:
"""Bootstrap the system by loading resources and starting controllers.
This is a convenience method that loads resources and starts the
orchestrator.
@@ -253,12 +253,12 @@
git_repos.append(resource)
elif isinstance(resource, Kustomization):
kustomizations.append(resource)
- except FluxException as e:
- _LOGGER.error("Failed to load initial resources: %s", e,
exc_info=True)
- return False
- except Exception as e:
- _LOGGER.error("Failed to load initial resources: %s", e,
exc_info=True)
- return False
+ except FluxException as err:
+ raise FluxException(f"Failed to load initial resources: {err}")
from err
+ except Exception as err:
+ raise FluxException(
+ f"Uncaught exception loading initial resources: {err}"
+ ) from err
# 2. Find the bootstrap GitRepository to associate with the local path
repo = git_repo.git_repo(options.path)
@@ -294,15 +294,15 @@
# 3. Start controllers and run
await self.start()
try:
- return await self.run()
+ await self.run()
finally:
await self.stop()
- async def run(self) -> bool:
+ async def run(self) -> None:
"""Run the orchestrator until all work is complete.
- Returns:
- bool: True if all work completed successfully, False if any
resources failed.
+ Raises:
+ FluxException: If the orchestrator fails or is cancelled.
"""
try:
await self.start()
@@ -310,24 +310,20 @@
# Wait for completion or error
while True:
if self.has_failed_resources():
- _LOGGER.error("Resource failures detected, stopping")
- return False
+ raise FluxException("Resource failures detected")
if self.is_complete():
_LOGGER.info("All work completed successfully")
- return True
+ return
# Allow tasks to run and avoid busy waiting
await get_task_service().block_till_done()
await asyncio.sleep(0.001)
-
- except asyncio.CancelledError:
- _LOGGER.info("Orchestrator was cancelled")
- return False
-
- except Exception as e:
- _LOGGER.exception("Orchestrator failed: %s", e)
- return False
-
+ except asyncio.CancelledError as err:
+ raise FluxException("Orchestrator was cancelled") from err
+ except FluxException:
+ raise
+ except Exception as err:
+ raise FluxException(f"Uncaught error in orchestrator: {err}") from
err
finally:
await self.stop()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/store/in_memory.py
new/flux_local-8.1.0/flux_local/store/in_memory.py
--- old/flux_local-8.0.1/flux_local/store/in_memory.py 2025-11-17
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/store/in_memory.py 2025-12-24
21:23:47.000000000 +0100
@@ -61,7 +61,8 @@
"Object %s already exists in store, skipping", resource_id
)
return
- _LOGGER.debug("Updating existing object %s in store", resource_id)
+ _LOGGER.info("Ignoring changes to existing object %s in store",
resource_id)
+ return
self._objects[resource_id] = obj
self._fire_event(StoreEvent.OBJECT_ADDED, resource_id, obj)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/build.py
new/flux_local-8.1.0/flux_local/tool/build.py
--- old/flux_local-8.0.1/flux_local/tool/build.py 2025-11-17
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build.py 2025-12-24
21:23:47.000000000 +0100
@@ -17,6 +17,9 @@
from .build_kustomization import (
BuildKustomizationAction as BuildKustomizationNewAction,
)
+from .build_helm import (
+ BuildHelmReleaseAction as BuildHelmReleaseNewAction,
+)
_LOGGER = logging.getLogger(__name__)
@@ -26,7 +29,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args = cast(
@@ -129,7 +133,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args: ArgumentParser = cast(
@@ -179,7 +184,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args: ArgumentParser = cast(
@@ -244,7 +250,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args: ArgumentParser = subparsers.add_parser(
@@ -261,6 +268,7 @@
BuildKustomizationAction.register(subcmds)
BuildKustomizationNewAction.register(subcmds)
BuildHelmReleaseAction.register(subcmds)
+ BuildHelmReleaseNewAction.register(subcmds)
BuildAllAction.register(subcmds)
args.set_defaults(cls=cls)
return args
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/build_common.py
new/flux_local-8.1.0/flux_local/tool/build_common.py
--- old/flux_local-8.0.1/flux_local/tool/build_common.py 1970-01-01
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build_common.py 2025-12-24
21:23:47.000000000 +0100
@@ -0,0 +1,159 @@
+"""Common build utilities for flux-local."""
+
+import logging
+import pathlib
+from typing import Any, Callable
+import yaml
+
+from flux_local.manifest import (
+ BaseManifest,
+ NamedResource,
+ strip_resource_attributes,
+ STRIP_ATTRIBUTES,
+ HelmRelease,
+ Kustomization,
+)
+from flux_local.helm_controller.artifact import HelmReleaseArtifact
+from flux_local.kustomize_controller.artifact import KustomizationArtifact
+from flux_local.exceptions import FluxException
+from flux_local.orchestrator import BootstrapOptions, Orchestrator,
OrchestratorConfig
+from flux_local.store import InMemoryStore, Status
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def filter_manifest(doc: dict[str, Any], **kwargs: Any) -> bool:
+ """Return true if the manifest should be included in the output."""
+ if kwargs.get("skip_crds") and doc.get("kind") ==
"CustomResourceDefinition":
+ return False
+ if kwargs.get("skip_secrets") and doc.get("kind") == "Secret":
+ return False
+ if (skip_kinds := kwargs.get("skip_kinds")) and isinstance(skip_kinds,
list):
+ if doc.get("kind") in skip_kinds:
+ return False
+ return True
+
+
+class BuildRunner:
+ """Common build runner for HelmReleases and Kustomizations."""
+
+ def __init__(
+ self,
+ config: OrchestratorConfig,
+ resource_kind: str,
+ resource_type: type[HelmRelease] | type[Kustomization],
+ artifact_type: type[HelmReleaseArtifact] | type[KustomizationArtifact],
+ selector_predicate: Callable[[BaseManifest], bool],
+ ) -> None:
+ self.config = config
+ self.resource_kind = resource_kind
+ self.resource_type = resource_type
+ self.artifact_type = artifact_type
+ self.selector_predicate = selector_predicate
+
+ def _process_manifest(
+ self, store: InMemoryStore, resource_id: NamedResource, **kwargs: Any
+ ) -> list[dict[str, Any]] | None:
+ """Process a single resource and return its manifests if ready."""
+ status = store.get_status(resource_id)
+ if not status:
+ _LOGGER.warning(
+ "%s %s has no status in the store", self.resource_kind,
resource_id
+ )
+ return None
+
+ if status.status != Status.READY:
+ _LOGGER.error(
+ "%s %s failed: %s", self.resource_kind, resource_id,
status.error
+ )
+ return None
+
+ artifact = store.get_artifact(resource_id, self.artifact_type)
+ if not artifact or not artifact.manifests:
+ _LOGGER.warning(
+ "%s %s is Ready but has no artifact or manifests",
+ self.resource_kind,
+ resource_id,
+ )
+ return None
+
+ _LOGGER.info(
+ "Found %d manifests for %s %s",
+ len(artifact.manifests),
+ self.resource_kind,
+ resource_id,
+ )
+ return [
+ manifest_item
+ for manifest_item in artifact.manifests
+ if filter_manifest(manifest_item, **kwargs)
+ ]
+
+ async def run(
+ self,
+ path: pathlib.Path,
+ output_file: str,
+ **kwargs: Any,
+ ) -> None:
+ """Async Action implementation."""
+ _LOGGER.info(
+ "Building %ss from path %s using new orchestrator",
self.resource_kind, path
+ )
+
+ store = InMemoryStore()
+ orchestrator = Orchestrator(store, self.config)
+ bootstrap_options = BootstrapOptions(path=path)
+ try:
+ await orchestrator.bootstrap(bootstrap_options)
+ except FluxException as err:
+ raise FluxException(
+ f"Orchestrator bootstrap failed for path {path}"
+ ) from err
+
+ manifest_found = False
+ manifest_match = False
+
+ with open(output_file, "w", encoding="utf-8") as file:
+ for manifest_obj in store.list_objects(kind=self.resource_kind):
+ if not isinstance(manifest_obj, self.resource_type):
+ continue
+ # We can remove these type ignores once we define
kind/name/namespace
+ # in the BaseManifest.
+ resource_id = NamedResource(
+ kind=manifest_obj.kind, # type: ignore[attr-defined]
+ name=manifest_obj.name, # type: ignore[attr-defined]
+ namespace=manifest_obj.namespace, # type:
ignore[attr-defined]
+ )
+
+ if not self.selector_predicate(manifest_obj):
+ _LOGGER.debug(
+ "%s %s did not match selector", self.resource_kind,
resource_id
+ )
+ continue
+
+ manifest_found = True
+ manifests = self._process_manifest(store, resource_id,
**kwargs)
+ if not manifests:
+ continue
+ manifest_match = True
+
+ for manifest_item in manifests:
+ strip_resource_attributes(
+ manifest_item,
+ STRIP_ATTRIBUTES,
+ )
+ yaml.dump(manifest_item, file, sort_keys=False,
explicit_start=True)
+
+ if not manifest_match:
+ if not manifest_found:
+ _LOGGER.warning(
+ "No %ss found or processed from path %s that matched
selector",
+ self.resource_kind,
+ path,
+ )
+ else:
+ _LOGGER.warning(
+ "No %ss that matched the selector were successfully
built from path %s",
+ self.resource_kind,
+ path,
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/build_helm.py
new/flux_local-8.1.0/flux_local/tool/build_helm.py
--- old/flux_local-8.0.1/flux_local/tool/build_helm.py 1970-01-01
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build_helm.py 2025-12-24
21:23:47.000000000 +0100
@@ -0,0 +1,100 @@
+"""Flux-local build for HelmReleases using the new Orchestrator."""
+
+import logging
+import pathlib
+from argparse import (
+ ArgumentParser,
+ _SubParsersAction as SubParsersAction,
+ BooleanOptionalAction,
+)
+from typing import Any, cast
+
+from flux_local import git_repo
+from flux_local.helm_controller.artifact import HelmReleaseArtifact
+from flux_local.manifest import (
+ HELM_RELEASE,
+ HelmRelease,
+)
+from flux_local.orchestrator import OrchestratorConfig
+
+from . import selector
+from .build_common import BuildRunner
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def build_hr_selector(path: pathlib.Path, **kwargs: Any) ->
git_repo.ResourceSelector:
+ """Build a HelmRelease selector from CLI arguments."""
+ cli_selector =
git_repo.ResourceSelector(path=git_repo.PathSelector(path=path))
+ cli_selector.helm_release.name = kwargs.get("helmrelease")
+ cli_selector.helm_release.namespace = kwargs.get("namespace")
+ cli_selector.helm_release.skip_crds = kwargs["skip_crds"]
+ cli_selector.helm_release.skip_secrets = kwargs["skip_secrets"]
+ cli_selector.helm_release.skip_kinds = kwargs.get("skip_kinds")
+ return cli_selector
+
+
+class BuildHelmReleaseAction:
+ """Flux-local build for HelmReleases using the new Orchestrator."""
+
+ @classmethod
+ def register(
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
+ ) -> ArgumentParser:
+ """Register the subparser commands."""
+ args: ArgumentParser = cast(
+ ArgumentParser,
+ subparsers.add_parser(
+ "helmreleases-new",
+ aliases=["hr-new"],
+ help="Build HelmRelease objects using the new experimental
Orchestrator",
+ description=(
+ "The build command uses the new orchestrator to build
HelmRelease objects."
+ ),
+ ),
+ )
+ args.add_argument(
+ "--output-file",
+ type=str,
+ default="/dev/stdout",
+ help="Output file for the results of the command",
+ )
+ args.add_argument(
+ "--wipe-secrets",
+ default=True,
+ action=BooleanOptionalAction,
+ help="Wipe secrets from the output",
+ )
+ args.add_argument(
+ "--enable-oci",
+ default=False,
+ action=BooleanOptionalAction,
+ help="Enable OCI repository sources",
+ )
+ selector.add_hr_selector_flags(args)
+ args.set_defaults(cls=cls)
+ return args
+
+ async def run(
+ self,
+ path: pathlib.Path,
+ output_file: str,
+ **kwargs: Any,
+ ) -> None:
+ """Async Action implementation."""
+ config = OrchestratorConfig(enable_helm=True)
+ config.kustomization_controller_config.wipe_secrets =
kwargs["wipe_secrets"]
+ config.read_action_config.wipe_secrets = kwargs["wipe_secrets"]
+ config.source_controller_config.enable_oci = kwargs["enable_oci"]
+
+ cli_selector = build_hr_selector(path, **kwargs)
+
+ runner = BuildRunner(
+ config=config,
+ resource_kind=HELM_RELEASE,
+ resource_type=HelmRelease,
+ artifact_type=HelmReleaseArtifact,
+ selector_predicate=cli_selector.helm_release.predicate, # type:
ignore[arg-type]
+ )
+ await runner.run(path, output_file, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/flux_local-8.0.1/flux_local/tool/build_kustomization.py
new/flux_local-8.1.0/flux_local/tool/build_kustomization.py
--- old/flux_local-8.0.1/flux_local/tool/build_kustomization.py 2025-11-17
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/build_kustomization.py 2025-12-24
21:23:47.000000000 +0100
@@ -8,21 +8,17 @@
BooleanOptionalAction,
)
from typing import Any, cast
-import yaml
from flux_local import git_repo
from flux_local.kustomize_controller.artifact import KustomizationArtifact
from flux_local.manifest import (
KUSTOMIZE_KIND,
Kustomization,
- NamedResource,
- strip_resource_attributes,
- STRIP_ATTRIBUTES,
)
-from flux_local.orchestrator import BootstrapOptions, Orchestrator,
OrchestratorConfig
-from flux_local.store import InMemoryStore, Status
+from flux_local.orchestrator import OrchestratorConfig
from . import selector
+from .build_common import BuildRunner
_LOGGER = logging.getLogger(__name__)
@@ -42,24 +38,13 @@
return cli_selector
-def filter_manifest(doc: dict[str, Any], **kwargs: Any) -> bool:
- """Return true if the manifest should be included in the output."""
- if kwargs["skip_crds"] and doc.get("kind") == "CustomResourceDefinition":
- return False
- if kwargs["skip_secrets"] and doc.get("kind") == "Secret":
- return False
- if (skip_kinds := kwargs.get("skip_kinds")) and isinstance(skip_kinds,
list):
- if doc.get("kind") in skip_kinds:
- return False
- return True
-
-
class BuildKustomizationAction:
"""Flux-local build for Kustomizations using the new Orchestrator."""
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args: ArgumentParser = cast(
@@ -95,38 +80,6 @@
args.set_defaults(cls=cls)
return args
- def _process_manifest(
- self, store: InMemoryStore, resource_id: NamedResource, **kwargs: Any
- ) -> list[dict[str, Any]] | None:
- """Process a single Kustomization and return its manifests if ready."""
- status = store.get_status(resource_id)
- if not status:
- _LOGGER.warning("Kustomization %s has no status in the store",
resource_id)
- return None
-
- if status.status != Status.READY:
- _LOGGER.error("Kustomization %s failed: %s", resource_id,
status.error)
- return None
-
- artifact = store.get_artifact(resource_id, KustomizationArtifact)
- if not artifact or not artifact.manifests:
- _LOGGER.warning(
- "Kustomization %s is Ready but has no artifact or manifests",
- resource_id,
- )
- return None
-
- _LOGGER.info(
- "Found %d manifests for Kustomization %s",
- len(artifact.manifests),
- resource_id,
- )
- return [
- manifest_item
- for manifest_item in artifact.manifests
- if filter_manifest(manifest_item, **kwargs)
- ]
-
async def run(
self,
path: pathlib.Path,
@@ -134,64 +87,19 @@
**kwargs: Any,
) -> None:
"""Async Action implementation."""
- _LOGGER.info(
- "Building Kustomizations from path %s using new orchestrator", path
- )
-
- store = InMemoryStore()
# Disable Helm for ks-only build
config = OrchestratorConfig(enable_helm=False)
config.kustomization_controller_config.wipe_secrets =
kwargs["wipe_secrets"]
config.read_action_config.wipe_secrets = kwargs["wipe_secrets"]
config.source_controller_config.enable_oci = kwargs["enable_oci"]
- orchestrator = Orchestrator(store, config)
- bootstrap_options = BootstrapOptions(path=path)
- if not await orchestrator.bootstrap(bootstrap_options):
- _LOGGER.error("Orchestrator bootstrap failed for path %s", path)
- return
cli_selector = build_ks_selector(path, **kwargs)
- manifest_found = False
- manifest_match = False
- is_match = cli_selector.kustomization.predicate
- with open(output_file, "w", encoding="utf-8") as file:
- for manifest_obj in store.list_objects(kind=KUSTOMIZE_KIND):
- if not isinstance(manifest_obj, Kustomization):
- continue
- resource_id = NamedResource(
- kind=manifest_obj.kind,
- name=manifest_obj.name,
- namespace=manifest_obj.namespace,
- )
-
- if not is_match(manifest_obj):
- _LOGGER.debug(
- "Kustomization %s did not match selector", resource_id
- )
- continue
-
- manifest_found = True
- manifests = self._process_manifest(store, resource_id,
**kwargs)
- if not manifests:
- continue
- manifest_match = True
-
- for manifest_item in manifests:
- strip_resource_attributes(
- manifest_item,
- STRIP_ATTRIBUTES,
- )
- yaml.dump(manifest_item, file, sort_keys=False,
explicit_start=True)
-
- if not manifest_match:
- if not manifest_found:
- _LOGGER.warning(
- "No Kustomizations found or processed from path %s
that matched selector",
- path,
- )
- else:
- _LOGGER.warning(
- "No Kustomizations that matched the selector were
successfully built from path %s",
- path,
- )
+ runner = BuildRunner(
+ config=config,
+ resource_kind=KUSTOMIZE_KIND,
+ resource_type=Kustomization,
+ artifact_type=KustomizationArtifact,
+ selector_predicate=cli_selector.kustomization.predicate, # type:
ignore[arg-type]
+ )
+ await runner.run(path, output_file, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/get.py
new/flux_local-8.1.0/flux_local/tool/get.py
--- old/flux_local-8.0.1/flux_local/tool/get.py 2025-11-17 06:09:18.000000000
+0100
+++ new/flux_local-8.1.0/flux_local/tool/get.py 2025-12-24 21:23:47.000000000
+0100
@@ -22,6 +22,8 @@
StructFormatter,
)
from . import selector
+from .get_kustomization import GetKustomizationNewAction
+from .get_helm import GetHelmReleaseNewAction
_LOGGER = logging.getLogger(__name__)
@@ -34,7 +36,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args = cast(
@@ -101,7 +104,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args = cast(
@@ -153,7 +157,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args = cast(
@@ -301,7 +306,8 @@
@classmethod
def register(
- cls, subparsers: SubParsersAction # type: ignore[type-arg]
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
) -> ArgumentParser:
"""Register the subparser commands."""
args = cast(
@@ -317,7 +323,9 @@
required=True,
)
GetKustomizationAction.register(subcmds)
+ GetKustomizationNewAction.register(subcmds)
GetHelmReleaseAction.register(subcmds)
+ GetHelmReleaseNewAction.register(subcmds)
GetClusterAction.register(subcmds)
args.set_defaults(cls=cls)
return args
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/get_common.py
new/flux_local-8.1.0/flux_local/tool/get_common.py
--- old/flux_local-8.0.1/flux_local/tool/get_common.py 1970-01-01
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/get_common.py 2025-12-24
21:23:47.000000000 +0100
@@ -0,0 +1,41 @@
+"""Common utilities for get commands."""
+
+import logging
+import pathlib
+from argparse import ArgumentParser, BooleanOptionalAction
+
+from flux_local.orchestrator import Orchestrator, OrchestratorConfig,
BootstrapOptions
+from flux_local.store import InMemoryStore
+from flux_local.exceptions import FluxException
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def add_common_flags(args: ArgumentParser) -> None:
+ """Add common flags to the arguments object."""
+ args.add_argument(
+ "--enable-oci",
+ default=False,
+ action=BooleanOptionalAction,
+ help="Enable OCI repository sources",
+ )
+
+
+async def bootstrap(path: pathlib.Path, enable_oci: bool) -> InMemoryStore:
+ """Bootstrap the orchestrator and return the store."""
+ # We don't need to enable helm controller to just list HelmReleases
+ # found in Kustomizations.
+ config = OrchestratorConfig(enable_helm=False)
+ config.source_controller_config.enable_oci = enable_oci
+
+ store = InMemoryStore()
+ orchestrator = Orchestrator(store, config)
+ bootstrap_options = BootstrapOptions(path=path)
+
+ try:
+ await orchestrator.bootstrap(bootstrap_options)
+ except FluxException:
+ # Continue to show what we found, even if some failed
+ pass
+
+ return store
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/get_helm.py
new/flux_local-8.1.0/flux_local/tool/get_helm.py
--- old/flux_local-8.0.1/flux_local/tool/get_helm.py 1970-01-01
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/get_helm.py 2025-12-24
21:23:47.000000000 +0100
@@ -0,0 +1,109 @@
+"""Flux-local get helmrelease action."""
+
+import logging
+import pathlib
+from argparse import (
+ ArgumentParser,
+ _SubParsersAction as SubParsersAction,
+)
+from typing import Any, cast
+
+from flux_local.kustomize_controller.artifact import KustomizationArtifact
+from flux_local.manifest import (
+ KUSTOMIZE_KIND,
+ Kustomization,
+ HelmRelease,
+ NamedResource,
+)
+
+from .format import PrintFormatter
+from . import selector, get_common
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class GetHelmReleaseNewAction:
+ """Get details about HelmReleases using the new Orchestrator."""
+
+ @classmethod
+ def register(
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
+ ) -> ArgumentParser:
+ """Register the subparser commands."""
+ args = cast(
+ ArgumentParser,
+ subparsers.add_parser(
+ "helmreleases-new",
+ aliases=["hr-new"],
+ help="Get HelmRelease objects using the new experimental
Orchestrator",
+ description="Print information about local flux HelmRelease
objects",
+ ),
+ )
+ selector.add_hr_selector_flags(args)
+ get_common.add_common_flags(args)
+ args.set_defaults(cls=cls)
+ return args
+
+ async def run(
+ self,
+ path: pathlib.Path | None,
+ **kwargs: Any,
+ ) -> None:
+ """Async Action implementation."""
+ if path is None:
+ path = pathlib.Path(".")
+
+ store = await get_common.bootstrap(
+ path, enable_oci=kwargs.get("enable_oci", False)
+ )
+
+ cli_selector = selector.build_hr_selector(**kwargs)
+
+ results: list[dict[str, Any]] = []
+ cols = ["name", "revision", "chart", "source"]
+ if cli_selector.helm_release.namespace is None:
+ cols.insert(0, "namespace")
+
+ for manifest_obj in store.list_objects(kind=KUSTOMIZE_KIND):
+ if not isinstance(manifest_obj, Kustomization):
+ continue
+
+ resource_id = NamedResource(
+ kind=manifest_obj.kind,
+ name=manifest_obj.name,
+ namespace=manifest_obj.namespace,
+ )
+
+ artifact = store.get_artifact(resource_id, KustomizationArtifact)
+ if not artifact:
+ continue
+
+ for doc in artifact.manifests:
+ if doc.get("kind") != "HelmRelease":
+ continue
+
+ try:
+ helm_release = HelmRelease.parse_doc(doc)
+ except Exception as e:
+ _LOGGER.debug("Failed to parse HelmRelease: %s", e)
+ continue
+
+ if not cli_selector.helm_release.predicate(helm_release):
+ continue
+
+ value = {
+ "name": helm_release.name,
+ "namespace": helm_release.namespace,
+ "revision": str(helm_release.chart.version),
+ "chart":
f"{helm_release.namespace}-{helm_release.chart.name}",
+ "source": helm_release.chart.repo_name,
+ }
+ results.append(value)
+
+ if not results:
+ print(selector.not_found("HelmRelease", cli_selector.helm_release))
+ return
+
+ PrintFormatter(cols).print(results)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/flux_local-8.0.1/flux_local/tool/get_kustomization.py
new/flux_local-8.1.0/flux_local/tool/get_kustomization.py
--- old/flux_local-8.0.1/flux_local/tool/get_kustomization.py 1970-01-01
01:00:00.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/get_kustomization.py 2025-12-24
21:23:47.000000000 +0100
@@ -0,0 +1,123 @@
+"""Flux-local get kustomization action."""
+
+import logging
+import pathlib
+from argparse import (
+ ArgumentParser,
+ _SubParsersAction as SubParsersAction,
+)
+from typing import Any, cast
+from collections import Counter
+
+from flux_local import git_repo
+from flux_local.kustomize_controller.artifact import KustomizationArtifact
+from flux_local.manifest import (
+ KUSTOMIZE_KIND,
+ Kustomization,
+ NamedResource,
+)
+
+from .format import PrintFormatter
+from . import selector, get_common
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class GetKustomizationNewAction:
+ """Get details about kustomizations using the new Orchestrator."""
+
+ @classmethod
+ def register(
+ cls,
+ subparsers: SubParsersAction, # type: ignore[type-arg]
+ ) -> ArgumentParser:
+ """Register the subparser commands."""
+ args = cast(
+ ArgumentParser,
+ subparsers.add_parser(
+ "kustomizations-new",
+ aliases=["ks-new"],
+ help="Get Kustomization objects using the new experimental
Orchestrator",
+ description="Print information about local flux Kustomization
objects",
+ ),
+ )
+ selector.add_ks_selector_flags(args)
+ args.add_argument(
+ "--output",
+ "-o",
+ choices=["wide"],
+ default=None,
+ help="Output format of the command",
+ )
+ get_common.add_common_flags(args)
+ args.set_defaults(cls=cls)
+ return args
+
+ async def run(
+ self,
+ path: pathlib.Path | None,
+ output: str | None,
+ **kwargs: Any,
+ ) -> None:
+ """Async Action implementation."""
+ if path is None:
+ path = pathlib.Path(".")
+
+ store = await get_common.bootstrap(
+ path, enable_oci=kwargs.get("enable_oci", False)
+ )
+
+ # We need to build the selector to filter results
+ # Reusing logic from build_kustomization.py
+ cli_selector =
git_repo.ResourceSelector(path=git_repo.PathSelector(path=path))
+ cli_selector.kustomization.name = kwargs.get("kustomization")
+ cli_selector.kustomization.namespace = kwargs.get("namespace")
+ if kwargs.get("all_namespaces"):
+ cli_selector.kustomization.namespace = None
+ cli_selector.kustomization.skip_crds = kwargs.get("skip_crds", False)
+ cli_selector.kustomization.skip_secrets = kwargs.get("skip_secrets",
False)
+ cli_selector.kustomization.skip_kinds = kwargs.get("skip_kinds")
+
+ results: list[dict[str, Any]] = []
+ cols = ["name", "path"]
+ if output == "wide":
+ cols.extend(["helmrepos", "ocirepos", "releases"])
+ if cli_selector.kustomization.namespace is None:
+ cols.insert(0, "namespace")
+
+ for manifest_obj in store.list_objects(kind=KUSTOMIZE_KIND):
+ if not isinstance(manifest_obj, Kustomization):
+ continue
+
+ # Filter
+ if not cli_selector.kustomization.predicate(manifest_obj):
+ continue
+
+ resource_id = NamedResource(
+ kind=manifest_obj.kind,
+ name=manifest_obj.name,
+ namespace=manifest_obj.namespace,
+ )
+
+ value: dict[str, str | int | None] = {
+ "name": manifest_obj.name,
+ "namespace": manifest_obj.namespace,
+ "path": manifest_obj.path,
+ }
+
+ if output == "wide":
+ artifact = store.get_artifact(resource_id,
KustomizationArtifact)
+ manifests = artifact.manifests if artifact else []
+ counts: Counter[str] = Counter(doc["kind"] for doc in
manifests)
+ value["helmrepos"] = counts["HelmRepository"]
+ value["ocirepos"] = counts["OCIRepository"]
+ value["releases"] = counts["HelmRelease"]
+
+ results.append(value)
+
+ if not results:
+ print(selector.not_found("Kustomization",
cli_selector.kustomization))
+ return
+
+ PrintFormatter(cols).print(results)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local/tool/test.py
new/flux_local-8.1.0/flux_local/tool/test.py
--- old/flux_local-8.0.1/flux_local/tool/test.py 2025-11-17
06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/flux_local/tool/test.py 2025-12-24
21:23:47.000000000 +0100
@@ -13,8 +13,6 @@
from pathlib import Path
import sys
from typing import cast, Generator, Any
-
-import nest_asyncio
import pytest
from flux_local import git_repo, kustomize
@@ -75,7 +73,6 @@
def runtest(self) -> None:
"""Dispatch the async work and run the test."""
- nest_asyncio.apply()
asyncio.run(self.async_runtest())
async def async_runtest(self) -> None:
@@ -138,7 +135,6 @@
def runtest(self) -> None:
"""Dispatch the async work and run the test."""
- nest_asyncio.apply()
asyncio.run(self.async_runtest())
async def async_runtest(self) -> None:
@@ -278,7 +274,6 @@
self.init_error: Exception | None = None
def pytest_sessionstart(self, session: pytest.Session) -> None:
- nest_asyncio.apply()
asyncio.run(self.async_pytest_sessionstart(session))
async def async_pytest_sessionstart(self, session: pytest.Session) -> None:
@@ -409,7 +404,6 @@
options = selector.options(**kwargs)
helm_options = selector.build_helm_options(**kwargs)
- nest_asyncio.apply()
pytest_args = [
"--verbosity",
str(verbosity),
@@ -417,9 +411,10 @@
"--disable-warnings",
]
_LOGGER.debug("pytest.main: %s", pytest_args)
- retcode = pytest.main(
+ retcode = await asyncio.to_thread(
+ pytest.main,
pytest_args,
- plugins=[
+ [
ManifestPlugin(
query,
TestConfig(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local.egg-info/PKG-INFO
new/flux_local-8.1.0/flux_local.egg-info/PKG-INFO
--- old/flux_local-8.0.1/flux_local.egg-info/PKG-INFO 2025-11-17
06:09:25.000000000 +0100
+++ new/flux_local-8.1.0/flux_local.egg-info/PKG-INFO 2025-12-24
21:23:53.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: flux_local
-Version: 8.0.1
+Version: 8.1.0
Summary: 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.
Author-email: Allen Porter <[email protected]>
License-Expression: Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/flux_local.egg-info/SOURCES.txt
new/flux_local-8.1.0/flux_local.egg-info/SOURCES.txt
--- old/flux_local-8.0.1/flux_local.egg-info/SOURCES.txt 2025-11-17
06:09:25.000000000 +0100
+++ new/flux_local-8.1.0/flux_local.egg-info/SOURCES.txt 2025-12-24
21:23:53.000000000 +0100
@@ -47,12 +47,17 @@
flux_local/task/service.py
flux_local/tool/__init__.py
flux_local/tool/build.py
+flux_local/tool/build_common.py
+flux_local/tool/build_helm.py
flux_local/tool/build_kustomization.py
flux_local/tool/diagnostics.py
flux_local/tool/diff.py
flux_local/tool/flux_local.py
flux_local/tool/format.py
flux_local/tool/get.py
+flux_local/tool/get_common.py
+flux_local/tool/get_helm.py
+flux_local/tool/get_kustomization.py
flux_local/tool/selector.py
flux_local/tool/test.py
flux_local/tool/shell/__init__.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/flux_local-8.0.1/pyproject.toml
new/flux_local-8.1.0/pyproject.toml
--- old/flux_local-8.0.1/pyproject.toml 2025-11-17 06:09:18.000000000 +0100
+++ new/flux_local-8.1.0/pyproject.toml 2025-12-24 21:23:47.000000000 +0100
@@ -4,7 +4,7 @@
[project]
name = "flux_local"
-version = "8.0.1"
+version = "8.1.0"
license = "Apache-2.0"
license-files = ["LICENSE"]
description = "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."