From 8983ff943320d24b0cb6a8c3b515ae3169a540cb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 21 Dec 2023 03:45:15 +0000 Subject: [PATCH 1/6] Add support for outputting container images from Kustomziations --- Dockerfile | 6 +- flux_local/git_repo.py | 89 +++++++++++++------ flux_local/image.py | 65 ++++++++++++++ flux_local/manifest.py | 3 + flux_local/tool/get.py | 13 +++ tests/__snapshots__/test_git_repo.ambr | 48 ++++++++++ tests/test_image.py | 41 +++++++++ tests/testdata/cluster8/README.md | 3 + .../testdata/cluster8/apps/kustomization.yaml | 5 ++ tests/testdata/cluster8/apps/pod-pvc.yaml | 19 ++++ tests/testdata/cluster8/cluster/apps.yaml | 18 ++++ .../cluster/flux-system/gotk-sync.yaml | 26 ++++++ .../cluster/flux-system/kustomization.yaml | 5 ++ .../tool/__snapshots__/test_get_cluster.ambr | 44 +++++++++ tests/tool/test_get_cluster.py | 4 +- 15 files changed, 358 insertions(+), 31 deletions(-) create mode 100644 flux_local/image.py create mode 100644 tests/test_image.py create mode 100644 tests/testdata/cluster8/README.md create mode 100644 tests/testdata/cluster8/apps/kustomization.yaml create mode 100644 tests/testdata/cluster8/apps/pod-pvc.yaml create mode 100644 tests/testdata/cluster8/cluster/apps.yaml create mode 100644 tests/testdata/cluster8/cluster/flux-system/gotk-sync.yaml create mode 100644 tests/testdata/cluster8/cluster/flux-system/kustomization.yaml diff --git a/Dockerfile b/Dockerfile index df9a7a52..2a769ddb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,9 +50,7 @@ RUN mkdir -p /src && \ rm -fr /src RUN flux version --client -COPY . /src/ -WORKDIR /src/ -RUN pip3 install -r /src/requirements.txt -RUN pip3 install -e /src/ +RUN pip3 install -r /workspaces/flux-local/requirements.txt +RUN pip3 install -e /workspaces/flux-local/ SHELL ["/bin/bash", "-c"] diff --git a/flux_local/git_repo.py b/flux_local/git_repo.py index 043079d4..e1e22416 100644 --- a/flux_local/git_repo.py +++ b/flux_local/git_repo.py @@ -218,6 +218,26 @@ class ResourceVisitor: """ +@dataclass +class DocumentVisitor: + """Invoked when a document is visited so the caller can intercept. + + This is similar to a resource visitor, but it visits the unparsed documents + since they may not have explicit schemas in this project. + """ + + kinds: list[str] + """The resource kinds of documents to visit.""" + + func: Callable[[str, dict[str, Any]], None] + """Function called with the resource and optional content. + + The function arguments are: + - parent: The namespaced name of the Fluxtomization or HelmRelease + - doc: The resource object (e.g. Pod, ConfigMap, HelmRelease, etc) + """ + + @dataclass class MetadataSelector: """A filter for objects to select from the cluster.""" @@ -304,6 +324,9 @@ class ResourceSelector: cluster_policy: MetadataSelector = field(default_factory=MetadataSelector) """ClusterPolicy objects to return.""" + doc_visitor: DocumentVisitor | None = None + """Raw objects to visit.""" + def is_allowed_source(sources: list[Source]) -> Callable[[Kustomization], bool]: """Return true if this Kustomization is from an allowed source.""" @@ -484,20 +507,23 @@ def node_name(ks: Kustomization) -> str: async def build_kustomization( kustomization: Kustomization, cluster_path: Path, - root: Path, - kustomization_selector: MetadataSelector, - helm_release_selector: MetadataSelector, - helm_repo_selector: MetadataSelector, - cluster_policy_selector: MetadataSelector, + selector: ResourceSelector, kustomize_flags: list[str], builder: CachableBuilder, ) -> tuple[Iterable[HelmRepository], Iterable[HelmRelease], Iterable[ClusterPolicy]]: """Build helm objects for the Kustomization.""" + + root: Path = selector.path.root + kustomization_selector: MetadataSelector = selector.kustomization + helm_repo_selector: MetadataSelector = selector.helm_repo + helm_release_selector: MetadataSelector = selector.helm_release + cluster_policy_selector: MetadataSelector = selector.cluster_policy if ( not kustomization_selector.enabled - and not helm_release_selector.enabled and not helm_repo_selector.enabled + and not helm_release_selector.enabled and not cluster_policy_selector.enabled + and not selector.doc_visitor ): return ([], [], []) @@ -519,23 +545,38 @@ async def build_kustomization( ) from err if kustomization_selector.visitor: - if kustomization_selector.visitor: - await kustomization_selector.visitor.func( - cluster_path, - Path(kustomization.path), - kustomization, - cmd, - ) + await kustomization_selector.visitor.func( + cluster_path, + Path(kustomization.path), + kustomization, + cmd, + ) - if ( - not helm_release_selector.enabled - and not helm_repo_selector.enabled - and not cluster_policy_selector.enabled - ): + kinds = [] + if helm_repo_selector.enabled: + kinds.append(HELM_REPO_KIND) + if helm_release_selector.enabled: + kinds.append(HELM_RELEASE_KIND) + if cluster_policy_selector.enabled: + kinds.append(CLUSTER_POLICY_KIND) + if selector.doc_visitor: + kinds.extend(selector.doc_visitor.kinds) + if not kinds: return ([], [], []) - docs = await cmd.grep( - f"kind=^({HELM_REPO_KIND}|{HELM_RELEASE_KIND}|{CLUSTER_POLICY_KIND})$" - ).objects(target_namespace=kustomization.target_namespace) + + regexp = f"kind=^({'|'.join(kinds)})$" + docs = await cmd.grep(regexp).objects( + target_namespace=kustomization.target_namespace + ) + + if selector.doc_visitor: + doc_kinds = set(selector.doc_visitor.kinds) + _LOGGER.debug(doc_kinds) + for doc in docs: + if doc.get("kind") not in doc_kinds: + continue + selector.doc_visitor.func(kustomization.namespaced_name, doc) + return ( filter( helm_repo_selector.predicate, @@ -609,11 +650,7 @@ async def update_kustomization(cluster: Cluster) -> None: build_kustomization( kustomization, Path(cluster.path), - selector.path.root, - selector.kustomization, - selector.helm_release, - selector.helm_repo, - selector.cluster_policy, + selector, options.kustomize_flags, builder, ) diff --git a/flux_local/image.py b/flux_local/image.py new file mode 100644 index 00000000..162c9fc3 --- /dev/null +++ b/flux_local/image.py @@ -0,0 +1,65 @@ +"""Helper functions for working with container images.""" + +import logging +from typing import Any + +from . import git_repo + +_LOGGER = logging.getLogger(__name__) + + +# Object types that may have container images. +KINDS = [ + "Pod", + "Deployment", + "StatefulSet", + "ReplicaSet", + "DaemonSet", + "CronJob", + "Job", + "ReplicationController", +] +IMAGE_KEY = "image" + + +def _extract_images(doc: dict[str, Any]) -> set[str]: + """Extract the image from a Kubernetes object.""" + images: set[str] = set({}) + for key, value in doc.items(): + if key == IMAGE_KEY: + images.add(value) + elif isinstance(value, dict): + images.update(_extract_images(value)) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + images.update(_extract_images(item)) + return images + + +class ImageVisitor: + """Helper that visits container image related objects. + + This tracks the container images used by the kustomizations and HelmReleases + so they can be dumped for further verification. + """ + + def __init__(self) -> None: + """Initialize ImageVisitor.""" + self.images: dict[str, set[str]] = {} + + def repo_visitor(self) -> git_repo.DocumentVisitor: + """Return a git_repo.DocumentVisitor that points to this object.""" + + def add_image(name: str, doc: dict[str, Any]) -> None: + """Visitor function to find relevant images and record them for later inspection. + + Updates the image set with the images found in the document. + """ + images = _extract_images(doc) + if name in self.images: + self.images[name].update(images) + else: + self.images[name] = set(images) + + return git_repo.DocumentVisitor(kinds=KINDS, func=add_image) diff --git a/flux_local/manifest.py b/flux_local/manifest.py index a083e173..7f0f6efd 100644 --- a/flux_local/manifest.py +++ b/flux_local/manifest.py @@ -293,6 +293,9 @@ class Kustomization(BaseManifest): contents: dict[str, Any] | None = None """Contents of the raw Kustomization document.""" + images: list[str] = Field(default_factory=list) + """The list of images referenced in the kustomization.""" + @classmethod def parse_doc(cls, doc: dict[str, Any]) -> "Kustomization": """Parse a partial Kustomization from a kubernetes resource.""" diff --git a/flux_local/tool/get.py b/flux_local/tool/get.py index 3ab8843c..4a4b722f 100644 --- a/flux_local/tool/get.py +++ b/flux_local/tool/get.py @@ -3,6 +3,7 @@ import logging from argparse import ArgumentParser, _SubParsersAction as SubParsersAction from typing import cast, Any +import sys from flux_local import git_repo @@ -149,6 +150,10 @@ def register( ), ) selector.add_cluster_selector_flags(args) + args.add_argument( + "--enable-images", + help="Output container images when traversing the cluster", + ) args.add_argument( "--output", "-o", @@ -162,11 +167,19 @@ def register( async def run( # type: ignore[no-untyped-def] self, output: str, + enable_images: bool, **kwargs, # pylint: disable=unused-argument ) -> None: """Async Action implementation.""" query = selector.build_cluster_selector(**kwargs) query.helm_release.enabled = output == "yaml" + if enable_images: + if output != "yaml": + print( + "Flag --enable-images only works with --output yaml", + file=sys.stderr, + ) + return manifest = await git_repo.build_manifest( selector=query, options=selector.options(**kwargs) ) diff --git a/tests/__snapshots__/test_git_repo.ambr b/tests/__snapshots__/test_git_repo.ambr index d7db3c23..73244d36 100644 --- a/tests/__snapshots__/test_git_repo.ambr +++ b/tests/__snapshots__/test_git_repo.ambr @@ -20,6 +20,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -31,6 +33,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -64,6 +68,8 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), + 'images': list([ + ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -93,6 +99,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -121,6 +129,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -132,6 +142,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -165,6 +177,8 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), + 'images': list([ + ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -176,6 +190,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -207,6 +223,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -218,6 +236,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -251,6 +271,8 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), + 'images': list([ + ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -280,6 +302,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -333,6 +357,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -344,6 +370,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -359,6 +387,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -388,6 +418,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -419,6 +451,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -430,6 +464,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -463,6 +499,8 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), + 'images': list([ + ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -492,6 +530,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -900,6 +940,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -911,6 +953,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -944,6 +988,8 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), + 'images': list([ + ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -973,6 +1019,8 @@ ]), 'helm_repos': list([ ]), + 'images': list([ + ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', diff --git a/tests/test_image.py b/tests/test_image.py new file mode 100644 index 00000000..613b4544 --- /dev/null +++ b/tests/test_image.py @@ -0,0 +1,41 @@ +"""Tests for image.""" + +from pathlib import Path +from typing import Any + +import pytest +from syrupy.assertion import SnapshotAssertion + +from flux_local.git_repo import build_manifest, ResourceSelector, PathSelector +from flux_local.image import ImageVisitor + +TESTDATA = Path("tests/testdata/cluster8") +CWD = Path.cwd() + + +@pytest.mark.parametrize( + ("test_path", "expected"), + [ + ( + CWD / "tests/testdata/cluster8", + {"flux-system/apps": {"alpine"}}, + ), + ( + CWD / "tests/testdata/cluster7", + {}, + ), + ], +) +async def test_image_visitor( + snapshot: SnapshotAssertion, test_path: str, expected: dict[str, Any] +) -> None: + """Tests for building the manifest.""" + + image_visitor = ImageVisitor() + query = ResourceSelector( + path=PathSelector(Path(test_path)), + doc_visitor=image_visitor.repo_visitor(), + ) + await build_manifest(selector=query) + + assert image_visitor.images == expected diff --git a/tests/testdata/cluster8/README.md b/tests/testdata/cluster8/README.md new file mode 100644 index 00000000..2b56dfc2 --- /dev/null +++ b/tests/testdata/cluster8/README.md @@ -0,0 +1,3 @@ +# cluster8 + +This cluster is meant to exercise the container image extraction conde. diff --git a/tests/testdata/cluster8/apps/kustomization.yaml b/tests/testdata/cluster8/apps/kustomization.yaml new file mode 100644 index 00000000..099b91b4 --- /dev/null +++ b/tests/testdata/cluster8/apps/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - pod-pvc.yaml diff --git a/tests/testdata/cluster8/apps/pod-pvc.yaml b/tests/testdata/cluster8/apps/pod-pvc.yaml new file mode 100644 index 00000000..13e9d1cf --- /dev/null +++ b/tests/testdata/cluster8/apps/pod-pvc.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: mnt-pod + namespace: home +spec: + volumes: + - name: task-pv-storage + persistentVolumeClaim: + claimName: some-pod-datadir + containers: + - name: task-pv-container + image: alpine + volumeMounts: + - mountPath: "/data" + name: task-pv-storage + command: ["/bin/sh"] + args: ["-c", "sleep 500000"] diff --git a/tests/testdata/cluster8/cluster/apps.yaml b/tests/testdata/cluster8/cluster/apps.yaml new file mode 100644 index 00000000..6d63b0e6 --- /dev/null +++ b/tests/testdata/cluster8/cluster/apps.yaml @@ -0,0 +1,18 @@ +--- +# Left on deprecated CRDs for testing +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: apps + namespace: flux-system +spec: + interval: 10m0s + dependsOn: + - name: infra-configs + sourceRef: + kind: GitRepository + name: flux-system + path: ./tests/testdata/cluster8/apps + prune: true + wait: true + timeout: 5m0s diff --git a/tests/testdata/cluster8/cluster/flux-system/gotk-sync.yaml b/tests/testdata/cluster8/cluster/flux-system/gotk-sync.yaml new file mode 100644 index 00000000..43cd28cb --- /dev/null +++ b/tests/testdata/cluster8/cluster/flux-system/gotk-sync.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 1m0s + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/allenporter/flux-local +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 10m0s + path: ./tests/testdata/cluster8/cluster/ + prune: true + sourceRef: + kind: GitRepository + name: flux-system diff --git a/tests/testdata/cluster8/cluster/flux-system/kustomization.yaml b/tests/testdata/cluster8/cluster/flux-system/kustomization.yaml new file mode 100644 index 00000000..197efd8a --- /dev/null +++ b/tests/testdata/cluster8/cluster/flux-system/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-sync.yaml diff --git a/tests/tool/__snapshots__/test_get_cluster.ambr b/tests/tool/__snapshots__/test_get_cluster.ambr index d42a9989..55c2806e 100644 --- a/tests/tool/__snapshots__/test_get_cluster.ambr +++ b/tests/tool/__snapshots__/test_get_cluster.ambr @@ -62,6 +62,46 @@ ''' # --- +# name: test_get_cluster[yaml-cluster7-images] + ''' + --- + clusters: + - path: tests/testdata/cluster7 + kustomizations: + - name: apps + namespace: flux-system + path: tests/testdata/cluster7/flux/apps + helm_repos: [] + helm_releases: + - name: postgresql + namespace: database + chart: + name: postgresql + repo_name: bitnami-charts + repo_namespace: flux-system + cluster_policies: [] + images: [] + - name: charts + namespace: flux-system + path: tests/testdata/cluster7/flux/charts + helm_repos: + - name: bitnami-charts + namespace: flux-system + url: oci://registry-1.docker.io/bitnamicharts + repo_type: oci + helm_releases: [] + cluster_policies: [] + images: [] + - name: flux-system + namespace: flux-system + path: tests/testdata/cluster7/clusters/home + helm_repos: [] + helm_releases: [] + cluster_policies: [] + images: [] + + ''' +# --- # name: test_get_cluster[yaml] ''' --- @@ -80,12 +120,14 @@ repo_name: podinfo repo_namespace: flux-system cluster_policies: [] + images: [] - name: flux-system namespace: flux-system path: tests/testdata/cluster/clusters/prod helm_repos: [] helm_releases: [] cluster_policies: [] + images: [] - name: infra-configs namespace: flux-system path: tests/testdata/cluster/infrastructure/configs @@ -106,6 +148,7 @@ cluster_policies: - name: test-allow-policy namespace: null + images: [] - name: infra-controllers namespace: flux-system path: tests/testdata/cluster/infrastructure/controllers @@ -124,6 +167,7 @@ repo_name: weave-charts repo_namespace: flux-system cluster_policies: [] + images: [] ''' # --- diff --git a/tests/tool/test_get_cluster.py b/tests/tool/test_get_cluster.py index 8f9f8b89..b55d8d7b 100644 --- a/tests/tool/test_get_cluster.py +++ b/tests/tool/test_get_cluster.py @@ -26,7 +26,8 @@ (["--path", "tests/testdata/cluster6"]), (["--path", "tests/testdata/cluster7"]), (["--all-namespaces", "--path", "tests/testdata/cluster/"]), - (["--path", "tests/testdata/cluster/", "-o", "yaml"]), + (["--path", "tests/testdata/cluster", "-o", "yaml"]), + (["--path", "tests/testdata/cluster7", "-o", "yaml"]), ], ids=[ "cluster", @@ -39,6 +40,7 @@ "cluster7", "all-namespaces", "yaml", + "yaml-cluster7-images" ], ) async def test_get_cluster(args: list[str], snapshot: SnapshotAssertion) -> None: From f5ba8f7a2cad90e557f67fc9eeb81a41e9521927 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 21 Dec 2023 04:02:41 +0000 Subject: [PATCH 2/6] Update cluster output with images --- flux_local/git_repo.py | 3 +-- flux_local/image.py | 11 +++++++- flux_local/tool/get.py | 14 +++++++++-- .../tool/__snapshots__/test_get_cluster.ambr | 25 +++---------------- tests/tool/test_get_cluster.py | 4 +-- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/flux_local/git_repo.py b/flux_local/git_repo.py index e1e22416..e56857a1 100644 --- a/flux_local/git_repo.py +++ b/flux_local/git_repo.py @@ -568,10 +568,9 @@ async def build_kustomization( docs = await cmd.grep(regexp).objects( target_namespace=kustomization.target_namespace ) - + if selector.doc_visitor: doc_kinds = set(selector.doc_visitor.kinds) - _LOGGER.debug(doc_kinds) for doc in docs: if doc.get("kind") not in doc_kinds: continue diff --git a/flux_local/image.py b/flux_local/image.py index 162c9fc3..0b5f9fc6 100644 --- a/flux_local/image.py +++ b/flux_local/image.py @@ -3,7 +3,7 @@ import logging from typing import Any -from . import git_repo +from . import git_repo, manifest _LOGGER = logging.getLogger(__name__) @@ -57,9 +57,18 @@ def add_image(name: str, doc: dict[str, Any]) -> None: Updates the image set with the images found in the document. """ images = _extract_images(doc) + if not images: + return if name in self.images: self.images[name].update(images) else: self.images[name] = set(images) return git_repo.DocumentVisitor(kinds=KINDS, func=add_image) + + def update_manifest(self, manifest: manifest.Manifest) -> None: + """Update the manifest with the images found in the repo.""" + for cluster in manifest.clusters: + for kustomization in cluster.kustomizations: + if images := self.images.get(kustomization.namespaced_name): + kustomization.images = list(images) diff --git a/flux_local/tool/get.py b/flux_local/tool/get.py index 4a4b722f..87e1eb1a 100644 --- a/flux_local/tool/get.py +++ b/flux_local/tool/get.py @@ -1,11 +1,11 @@ """Flux-local get action.""" import logging -from argparse import ArgumentParser, _SubParsersAction as SubParsersAction +from argparse import ArgumentParser, BooleanOptionalAction, _SubParsersAction as SubParsersAction from typing import cast, Any import sys -from flux_local import git_repo +from flux_local import git_repo, image from .format import PrintFormatter, YamlFormatter from . import selector @@ -152,6 +152,9 @@ def register( selector.add_cluster_selector_flags(args) args.add_argument( "--enable-images", + type=str, + default=False, + action=BooleanOptionalAction, help="Output container images when traversing the cluster", ) args.add_argument( @@ -173,6 +176,8 @@ async def run( # type: ignore[no-untyped-def] """Async Action implementation.""" query = selector.build_cluster_selector(**kwargs) query.helm_release.enabled = output == "yaml" + + image_visitor: image.ImageVisitor | None = None if enable_images: if output != "yaml": print( @@ -180,10 +185,15 @@ async def run( # type: ignore[no-untyped-def] file=sys.stderr, ) return + image_visitor = image.ImageVisitor() + query.doc_visitor = image_visitor.repo_visitor() + manifest = await git_repo.build_manifest( selector=query, options=selector.options(**kwargs) ) if output == "yaml": + if image_visitor: + image_visitor.update_manifest(manifest) YamlFormatter().print([manifest.compact_dict()]) return diff --git a/tests/tool/__snapshots__/test_get_cluster.ambr b/tests/tool/__snapshots__/test_get_cluster.ambr index 55c2806e..594d1198 100644 --- a/tests/tool/__snapshots__/test_get_cluster.ambr +++ b/tests/tool/__snapshots__/test_get_cluster.ambr @@ -62,39 +62,22 @@ ''' # --- -# name: test_get_cluster[yaml-cluster7-images] +# name: test_get_cluster[yaml-cluster8-images] ''' --- clusters: - - path: tests/testdata/cluster7 + - path: tests/testdata/cluster8 kustomizations: - name: apps namespace: flux-system - path: tests/testdata/cluster7/flux/apps + path: tests/testdata/cluster8/apps helm_repos: [] - helm_releases: - - name: postgresql - namespace: database - chart: - name: postgresql - repo_name: bitnami-charts - repo_namespace: flux-system - cluster_policies: [] - images: [] - - name: charts - namespace: flux-system - path: tests/testdata/cluster7/flux/charts - helm_repos: - - name: bitnami-charts - namespace: flux-system - url: oci://registry-1.docker.io/bitnamicharts - repo_type: oci helm_releases: [] cluster_policies: [] images: [] - name: flux-system namespace: flux-system - path: tests/testdata/cluster7/clusters/home + path: tests/testdata/cluster8/cluster helm_repos: [] helm_releases: [] cluster_policies: [] diff --git a/tests/tool/test_get_cluster.py b/tests/tool/test_get_cluster.py index b55d8d7b..5ebeb9f0 100644 --- a/tests/tool/test_get_cluster.py +++ b/tests/tool/test_get_cluster.py @@ -27,7 +27,7 @@ (["--path", "tests/testdata/cluster7"]), (["--all-namespaces", "--path", "tests/testdata/cluster/"]), (["--path", "tests/testdata/cluster", "-o", "yaml"]), - (["--path", "tests/testdata/cluster7", "-o", "yaml"]), + (["--path", "tests/testdata/cluster8", "-o", "yaml"]), ], ids=[ "cluster", @@ -40,7 +40,7 @@ "cluster7", "all-namespaces", "yaml", - "yaml-cluster7-images" + "yaml-cluster8-images" ], ) async def test_get_cluster(args: list[str], snapshot: SnapshotAssertion) -> None: From 8a658716d7820080a4348f71e0c1723a0d95574b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 21 Dec 2023 04:07:24 +0000 Subject: [PATCH 3/6] Add additional test coverage for mutliple images --- tests/test_image.py | 2 +- tests/testdata/cluster8/apps/kustomization.yaml | 2 +- .../cluster8/apps/{pod-pvc.yaml => pods.yaml} | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) rename tests/testdata/cluster8/apps/{pod-pvc.yaml => pods.yaml} (66%) diff --git a/tests/test_image.py b/tests/test_image.py index 613b4544..7aa850b3 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -18,7 +18,7 @@ [ ( CWD / "tests/testdata/cluster8", - {"flux-system/apps": {"alpine"}}, + {"flux-system/apps": {"alpine", "busybox"}}, ), ( CWD / "tests/testdata/cluster7", diff --git a/tests/testdata/cluster8/apps/kustomization.yaml b/tests/testdata/cluster8/apps/kustomization.yaml index 099b91b4..1673237b 100644 --- a/tests/testdata/cluster8/apps/kustomization.yaml +++ b/tests/testdata/cluster8/apps/kustomization.yaml @@ -2,4 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - pod-pvc.yaml + - pods.yaml diff --git a/tests/testdata/cluster8/apps/pod-pvc.yaml b/tests/testdata/cluster8/apps/pods.yaml similarity index 66% rename from tests/testdata/cluster8/apps/pod-pvc.yaml rename to tests/testdata/cluster8/apps/pods.yaml index 13e9d1cf..73e7d7e2 100644 --- a/tests/testdata/cluster8/apps/pod-pvc.yaml +++ b/tests/testdata/cluster8/apps/pods.yaml @@ -17,3 +17,15 @@ spec: name: task-pv-storage command: ["/bin/sh"] args: ["-c", "sleep 500000"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: sleep + namespace: home +spec: + initContainers: + - name: sleep + image: busybox + command: ["/bin/sh"] + args: ["-c", "sleep 500000"] \ No newline at end of file From 860b9e8a3a845e9c82f3fdd4dacc35bc05c46ea8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 21 Dec 2023 04:08:07 +0000 Subject: [PATCH 4/6] Revert dockerfile change --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2a769ddb..df9a7a52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,9 @@ RUN mkdir -p /src && \ rm -fr /src RUN flux version --client -RUN pip3 install -r /workspaces/flux-local/requirements.txt -RUN pip3 install -e /workspaces/flux-local/ +COPY . /src/ +WORKDIR /src/ +RUN pip3 install -r /src/requirements.txt +RUN pip3 install -e /src/ SHELL ["/bin/bash", "-c"] From 1464945ad4169a92dd686948732f38dc43ea566d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 21 Dec 2023 04:27:19 +0000 Subject: [PATCH 5/6] Exclude unset values in manifest --- flux_local/manifest.py | 4 ++-- flux_local/tool/get.py | 13 ++++++++++++- tests/testdata/cluster8/apps/pods.yaml | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/flux_local/manifest.py b/flux_local/manifest.py index 7f0f6efd..a26c5998 100644 --- a/flux_local/manifest.py +++ b/flux_local/manifest.py @@ -57,7 +57,7 @@ class BaseManifest(BaseModel): _COMPACT_EXCLUDE_FIELDS: dict[str, Any] = {} - def compact_dict(self, exclude: dict[str, Any] | None = None) -> dict[str, Any]: + def compact_dict(self, exclude: dict[str, Any] | None = None, include: dict[str, Any] | None = None) -> dict[str, Any]: """Return a compact dictionary representation of the object. This is similar to `dict()` but with a specific implementation for serializing @@ -65,7 +65,7 @@ def compact_dict(self, exclude: dict[str, Any] | None = None) -> dict[str, Any]: """ if exclude is None: exclude = self._COMPACT_EXCLUDE_FIELDS - return self.dict(exclude=exclude) # type: ignore[arg-type] + return self.dict(exclude=exclude, exclude_unset=True, exclude_none=True, exclude_defaults=True) # type: ignore[arg-type] @classmethod def parse_yaml(cls, content: str) -> "BaseManifest": diff --git a/flux_local/tool/get.py b/flux_local/tool/get.py index 87e1eb1a..3d2024d5 100644 --- a/flux_local/tool/get.py +++ b/flux_local/tool/get.py @@ -192,9 +192,20 @@ async def run( # type: ignore[no-untyped-def] selector=query, options=selector.options(**kwargs) ) if output == "yaml": + include: dict[str, Any] | None = None if image_visitor: image_visitor.update_manifest(manifest) - YamlFormatter().print([manifest.compact_dict()]) + include = { + "clusters": { + "__all__": { + "kustomizations": { + "__all__": True, + #"images": True, + } + } + } + } + YamlFormatter().print([manifest.compact_dict(include=include)]) return cols = ["path", "kustomizations"] diff --git a/tests/testdata/cluster8/apps/pods.yaml b/tests/testdata/cluster8/apps/pods.yaml index 73e7d7e2..05d8839a 100644 --- a/tests/testdata/cluster8/apps/pods.yaml +++ b/tests/testdata/cluster8/apps/pods.yaml @@ -28,4 +28,4 @@ spec: - name: sleep image: busybox command: ["/bin/sh"] - args: ["-c", "sleep 500000"] \ No newline at end of file + args: ["-c", "sleep 500000"] From 6a44ea6fa652c11f905196fee49c6f7bae7c1f40 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 21 Dec 2023 04:32:40 +0000 Subject: [PATCH 6/6] Update tests to omit default values --- tests/__snapshots__/test_git_repo.ambr | 54 ------------------- .../tool/__snapshots__/test_get_cluster.ambr | 7 --- 2 files changed, 61 deletions(-) diff --git a/tests/__snapshots__/test_git_repo.ambr b/tests/__snapshots__/test_git_repo.ambr index 73244d36..614a4ced 100644 --- a/tests/__snapshots__/test_git_repo.ambr +++ b/tests/__snapshots__/test_git_repo.ambr @@ -20,8 +20,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -33,8 +31,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -43,7 +39,6 @@ 'cluster_policies': list([ dict({ 'name': 'test-allow-policy', - 'namespace': None, }), ]), 'helm_releases': list([ @@ -68,8 +63,6 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), - 'images': list([ - ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -99,8 +92,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -129,8 +120,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -142,8 +131,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -152,7 +139,6 @@ 'cluster_policies': list([ dict({ 'name': 'test-allow-policy', - 'namespace': None, }), ]), 'helm_releases': list([ @@ -177,8 +163,6 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), - 'images': list([ - ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -190,8 +174,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -223,8 +205,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -236,8 +216,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -246,7 +224,6 @@ 'cluster_policies': list([ dict({ 'name': 'test-allow-policy', - 'namespace': None, }), ]), 'helm_releases': list([ @@ -271,8 +248,6 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), - 'images': list([ - ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -302,8 +277,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -357,8 +330,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -370,8 +341,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -380,15 +349,12 @@ 'cluster_policies': list([ dict({ 'name': 'test-allow-policy', - 'namespace': None, }), ]), 'helm_releases': list([ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -418,8 +384,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -451,8 +415,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -464,8 +426,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -474,7 +434,6 @@ 'cluster_policies': list([ dict({ 'name': 'test-allow-policy', - 'namespace': None, }), ]), 'helm_releases': list([ @@ -499,8 +458,6 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), - 'images': list([ - ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -530,8 +487,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', @@ -940,8 +895,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'apps', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/apps/prod', @@ -953,8 +906,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'flux-system', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/clusters/prod', @@ -963,7 +914,6 @@ 'cluster_policies': list([ dict({ 'name': 'test-allow-policy', - 'namespace': None, }), ]), 'helm_releases': list([ @@ -988,8 +938,6 @@ 'url': 'oci://ghcr.io/weaveworks/charts', }), ]), - 'images': list([ - ]), 'name': 'infra-configs', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/configs', @@ -1019,8 +967,6 @@ ]), 'helm_repos': list([ ]), - 'images': list([ - ]), 'name': 'infra-controllers', 'namespace': 'flux-system', 'path': 'tests/testdata/cluster/infrastructure/controllers', diff --git a/tests/tool/__snapshots__/test_get_cluster.ambr b/tests/tool/__snapshots__/test_get_cluster.ambr index 594d1198..fc0a4bac 100644 --- a/tests/tool/__snapshots__/test_get_cluster.ambr +++ b/tests/tool/__snapshots__/test_get_cluster.ambr @@ -74,14 +74,12 @@ helm_repos: [] helm_releases: [] cluster_policies: [] - images: [] - name: flux-system namespace: flux-system path: tests/testdata/cluster8/cluster helm_repos: [] helm_releases: [] cluster_policies: [] - images: [] ''' # --- @@ -103,14 +101,12 @@ repo_name: podinfo repo_namespace: flux-system cluster_policies: [] - images: [] - name: flux-system namespace: flux-system path: tests/testdata/cluster/clusters/prod helm_repos: [] helm_releases: [] cluster_policies: [] - images: [] - name: infra-configs namespace: flux-system path: tests/testdata/cluster/infrastructure/configs @@ -130,8 +126,6 @@ helm_releases: [] cluster_policies: - name: test-allow-policy - namespace: null - images: [] - name: infra-controllers namespace: flux-system path: tests/testdata/cluster/infrastructure/controllers @@ -150,7 +144,6 @@ repo_name: weave-charts repo_namespace: flux-system cluster_policies: [] - images: [] ''' # ---