diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..99e851cb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,24 @@ +# Docker environment for local development in devcontainer + +FROM ubuntu:jammy-20231128 + +RUN apt-get update --fix-missing && \ + apt-get upgrade -y && \ + apt-get install -y --fix-missing \ + curl \ + unzip \ + software-properties-common \ + vim \ + git \ + python3-pip + +COPY --from=docker.io/bitnami/kubectl:1.29.0 /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/kubectl +COPY --from=registry.k8s.io/kustomize/kustomize:v5.3.0 /app/kustomize /usr/local/bin/kustomize +COPY --from=ghcr.io/kyverno/kyverno-cli:v1.11.3 /ko-app/kubectl-kyverno /usr/local/bin/kyverno +COPY --from=docker.io/alpine/helm:3.13.3 /usr/bin/helm /usr/local/bin/helm +COPY --from=ghcr.io/fluxcd/flux-cli:v2.2.2 /usr/local/bin/flux /usr/local/bin/flux + +COPY requirements.txt /src/ +RUN pip3 install -r /src/requirements.txt + +SHELL ["/bin/bash", "-c"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 677eaf80..f0a2f26a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,9 +1,13 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/kubernetes-helm { "name": "Kubernetes - Local Configuration", "build": { "context": "..", - "dockerfile": "../Dockerfile" - } + "dockerfile": "./Dockerfile" + }, + // Creates a local volume where you the developer need to clone the git + // repo inside the container. Uses a local volume since this project + // relies heavily on local disk performance. + //"workspaceMount": "source=flux-local,target=/workspaces,type=volume", + //"workspaceFolder": "/workspaces/", + //"postCreateCommand": "chown vscode /workspaces", } diff --git a/.github/workflows/container-release.yaml b/.github/workflows/container-release.yaml new file mode 100644 index 00000000..33c92105 --- /dev/null +++ b/.github/workflows/container-release.yaml @@ -0,0 +1,53 @@ +--- +name: Container Release + +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + flavor: | + latest=true + prefix=v + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/container-test.yaml b/.github/workflows/container-test.yaml new file mode 100644 index 00000000..744f4771 --- /dev/null +++ b/.github/workflows/container-test.yaml @@ -0,0 +1,52 @@ +--- +name: Container Test + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + if: ${{ github.event.pull_request.head.repo.full_name == 'allenporter/flux-local' || github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=pr + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/flux-local-diff.yaml b/.github/workflows/flux-local-diff.yaml index 81787297..c338d808 100644 --- a/.github/workflows/flux-local-diff.yaml +++ b/.github/workflows/flux-local-diff.yaml @@ -28,6 +28,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Flux CLI + uses: fluxcd/flux2/action@v2.2.2 - uses: ./action/diff id: diff with: diff --git a/.github/workflows/flux-local-test.yaml b/.github/workflows/flux-local-test.yaml index a03b43c6..f73e1094 100644 --- a/.github/workflows/flux-local-test.yaml +++ b/.github/workflows/flux-local-test.yaml @@ -24,6 +24,8 @@ jobs: - tests/testdata/cluster7 steps: - uses: actions/checkout@v4 + - name: Setup Flux CLI + uses: fluxcd/flux2/action@v2.2.2 - uses: ./action/test with: enable-helm: true diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index f527a7e7..7219448f 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -15,6 +15,7 @@ permissions: contents: read pages: write id-token: write + actions: read # Allow one concurrent deployment concurrency: @@ -30,25 +31,23 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - python-version: ["3.10"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: '3.x' - name: Install dependencies run: | pip install -r requirements.txt - run: pdoc ./flux_local -o docs/ - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: # Upload entire repository path: 'docs/' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml index 428149f9..04365ee3 100644 --- a/.github/workflows/python-package.yaml +++ b/.github/workflows/python-package.yaml @@ -19,29 +19,28 @@ jobs: python-version: - "3.10" - "3.11" + - "3.12" steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - name: Setup Flux CLI + uses: fluxcd/flux2/action@v2.2.2 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "pip" + cache: pip - name: Install dependencies run: | - python -m pip install --upgrade pip pip install -r requirements.txt + pip install -e . - uses: supplypike/setup-bin@v3 with: - uri: https://github.com/kyverno/kyverno/releases/download/v1.9.0/kyverno-cli_v1.9.0_linux_x86_64.tar.gz + uri: https://github.com/kyverno/kyverno/releases/download/v1.11.3/kyverno-cli_v1.11.3_linux_x86_64.tar.gz name: kyverno-cli - version: v1.9.0 + version: v1.11.3 - name: Test with pytest run: | - SKIP_DIFF_TESTS=1 pytest --cov=flux_local --cov-report=term-missing - - name: Test with pytest pydantic v1 - run: | - pip install pydantic==1.10.11 - SKIP_DIFF_TESTS=1 pytest --cov=flux_local --cov-report=term-missing + SKIP_DIFF_TESTS=1 pytest --cov=flux_local --cov-report=term-missing --snapshot-warn-unused - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/python-publish.yaml b/.github/workflows/python-publish.yaml index 6aeb2b01..89edf819 100644 --- a/.github/workflows/python-publish.yaml +++ b/.github/workflows/python-publish.yaml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies @@ -26,7 +26,7 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40a9a7e8..e6d93045 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,34 +4,34 @@ repos: rev: v4.5.0 hooks: - id: trailing-whitespace - exclude: '^tests/tool/testdata/.*\.yaml$' + exclude: '^tests/.*.ambr$' - id: end-of-file-fixer - exclude: '^tests/tool/testdata/.*\.yaml$' + exclude: '^tests/.*.ambr$' - id: check-yaml args: - --allow-multiple-documents - id: check-added-large-files - repo: https://github.com/codespell-project/codespell - rev: v2.2.5 + rev: v2.2.6 hooks: - id: codespell - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.292' + rev: 'v0.1.11' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/adrienverge/yamllint.git - rev: v1.32.0 + rev: v1.33.0 hooks: - id: yamllint - exclude: '^tests/tool/testdata/.*\.yaml$' args: - -c - ".yaml-lint.yaml" - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.12.1 hooks: - id: black + exclude: '^tests/.*.ambr$' - repo: local hooks: - id: mypy diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a97d6a3..d4a6439b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,9 +24,9 @@ To run the tests locally, you can use the following command: $ pytest ``` -Some tests used `pytest-golden` to test against golden files. These can be updated with: +Some tests used `syrup` to test against golden snapshot files. These can be updated with: ```bash -$ pytest --update-goldens +$ pytest --snapshot-update ``` ## Documentation diff --git a/Dockerfile b/Dockerfile index 998ff802..b74a0d25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,21 @@ -# Docker environment for local development in devcontainer -FROM ubuntu:jammy-20231004 +FROM python:3.12-alpine as base -RUN apt-get update --fix-missing && \ - apt-get upgrade -y && \ - apt-get install -y --fix-missing \ - curl \ - unzip \ - software-properties-common \ - vim \ - git \ - python3-pip +RUN apk add --no-cache ca-certificates git -# renovate: datasource=github-releases depName=kubernetes-sigs/kustomize -ARG KUSTOMIZE_VERSION=v5.0.3 -RUN cd /usr/local/bin/ && \ - curl -OL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz && \ - tar xf kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz && \ - chmod +x kustomize -RUN kustomize version +WORKDIR /app +COPY requirements.txt /requirements.txt +COPY flux_local/ ./flux_local +COPY setup.py . +COPY setup.cfg . -# renovate: datasource=github-releases depName=helm/helm -ARG HELM_CLI_VERSION=v3.12.1 -RUN mkdir -p /src && \ - cd /src && \ - curl -OL https://get.helm.sh/helm-${HELM_CLI_VERSION}-linux-amd64.tar.gz && \ - tar xf helm-${HELM_CLI_VERSION}-linux-amd64.tar.gz && \ - cp linux-amd64/helm /usr/local/bin/helm && \ - rm -fr /src -RUN helm version +RUN pip install --no-cache-dir -r /requirements.txt +RUN pip install -e . -# renovate: datasource=github-releases depName=kyverno/kyverno -ARG KYVERNO_VERSION=v1.10.0 -RUN mkdir -p /src && \ - cd /src && \ - curl -OL https://github.com/kyverno/kyverno/releases/download/${KYVERNO_VERSION}/kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz && \ - tar xf kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz && \ - cp kyverno /usr/local/bin/kyverno && \ - chmod +x /usr/local/bin/kyverno && \ - rm -fr /src -RUN kyverno version +COPY --from=ghcr.io/fluxcd/flux-cli:v2.2.2 /usr/local/bin/flux /usr/local/bin/flux +COPY --from=docker.io/alpine/helm:3.13.3 /usr/bin/helm /usr/local/bin/helm +COPY --from=docker.io/bitnami/kubectl:1.29.0 /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/kubectl +COPY --from=registry.k8s.io/kustomize/kustomize:v5.3.0 /app/kustomize /usr/local/bin/kustomize +COPY --from=ghcr.io/kyverno/kyverno-cli:v1.11.3 /ko-app/kubectl-kyverno /usr/local/bin/kyverno -COPY . /src/ -WORKDIR /src/ -RUN pip3 install -r /src/requirements.txt - -SHELL ["/bin/bash", "-c"] \ No newline at end of file +USER 1001 +ENTRYPOINT ["/usr/local/bin/flux-local"] diff --git a/README.md b/README.md index 8e0f1e63..4e964251 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ clusters/prod ............................................ ## GitHub Action You may use `flux-local` as a github action to verify the health of the cluster on changes -or PRs. +or PRs. The actions expect to find the `flux` and `kustomize` binaries installed. ### test action @@ -250,6 +250,8 @@ This example will run `flux-local test` against the cluster in `clusters/prod` w helm release expansion enabled. ```yaml +- name: Setup Flux CLI + uses: fluxcd/flux2/action@v2 - uses: allenporter/flux-local/test@2.0.0 with: path: clusters/prod @@ -267,6 +269,8 @@ clusters showing you the final output. This is an example that diffs a `HelmRelease`: ```yaml +- name: Setup Flux CLI + uses: fluxcd/flux2/action@v2 - uses: allenporter/flux-local/action/diff@2.0.0 id: diff with: @@ -302,6 +306,8 @@ jobs: - helmrelease - kustomization steps: + - name: Setup Flux CLI + uses: fluxcd/flux2/action@v2 - uses: allenporter/flux-local/action/diff@2.0.0 id: diff with: diff --git a/action/diff/action.yml b/action/diff/action.yml index 3f9b662d..fb8b665e 100644 --- a/action/diff/action.yml +++ b/action/diff/action.yml @@ -50,17 +50,31 @@ inputs: outputs: diff: description: Output of the diff command or empty if there is no diff - value: ${{ steps.flux_diff.outputs.diff }} + value: ${{ steps.flux_diff_output.outputs.diff }} runs: using: "composite" steps: + - name: Verify Flux CLI + run: + flux --version || (echo "Could not find flux CLI, add https://fluxcd.io/flux/flux-gh-action/" && exit 1) + shell: bash + - name: Copy requirements locally + id: copy-requirements + shell: bash + run: | + # `cache-dependency-path` seems to need to be within the current directory. Use a temporary directory + tempdir=$(mktemp --directory --tmpdir=. --suffix=-flux-local-diff-action) + cp ${{ github.action_path }}/../../requirements.txt $tempdir + echo "::set-output name=directory::${tempdir}" - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - - name: Install flux-local + cache: pip + cache-dependency-path: ${{ steps.copy-requirements.outputs.directory }}/requirements.txt + - name: Install flux-local and requirements run: | - python -m pip install --upgrade pip + pip install -r ${{ steps.copy-requirements.outputs.directory }}/requirements.txt pip install -e ${{ github.action_path }}/../../ shell: bash - name: Checkout PR branch @@ -74,11 +88,9 @@ runs: ref: ${{ inputs.live-branch }} path: live token: ${{ inputs.token }} - - name: flux-local diff + - name: Diff Resources id: flux_diff run: | - delimiter="$(openssl rand -hex 8)" - echo "diff<<${delimiter}" >> $GITHUB_OUTPUT if [[ "${{ inputs.resource }}" == "helmrelease" ]]; then extra_flags="--api-versions=${{ inputs.api-versions}}" else @@ -98,7 +110,14 @@ runs: --all-namespaces \ --kustomize-build-flags="${{ inputs.kustomize-build-flags }}" \ --sources "${{ inputs.sources }}" \ - ${extra_flags} \ - >> $GITHUB_OUTPUT + --output-file diff.patch \ + ${extra_flags} + shell: bash + - name: Generate Diff output + id: flux_diff_output + run: | + delimiter="$(openssl rand -hex 8)" + echo "diff<<${delimiter}" >> $GITHUB_OUTPUT + cat diff.patch >> $GITHUB_OUTPUT echo "${delimiter}" >> $GITHUB_OUTPUT shell: bash diff --git a/action/test/action.yml b/action/test/action.yml index cafe304f..3aafbeda 100644 --- a/action/test/action.yml +++ b/action/test/action.yml @@ -35,13 +35,27 @@ inputs: runs: using: "composite" steps: + - name: Verify Flux CLI + run: + flux --version || (echo "Could not find flux CLI, add https://fluxcd.io/flux/flux-gh-action/" && exit 1) + shell: bash + - name: Copy requirements locally + id: copy-requirements + shell: bash + run: | + # `cache-dependency-path` seems to need to be within the current directory. Use a temporary directory + tempdir=$(mktemp --directory --tmpdir=. --suffix=-flux-local-diff-action) + cp ${{ github.action_path }}/../../requirements.txt $tempdir + echo "::set-output name=directory::${tempdir}" - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - - name: Install flux-local + cache: pip + cache-dependency-path: ${{ steps.copy-requirements.outputs.directory }}/requirements.txt + - name: Install flux-local and requirements run: | - python -m pip install --upgrade pip + pip install -r ${{ steps.copy-requirements.outputs.directory }}/requirements.txt pip install -e ${{ github.action_path }}/../../ shell: bash - uses: supplypike/setup-bin@v3 diff --git a/flux_local/command.py b/flux_local/command.py index 7ae8e4bc..15652014 100644 --- a/flux_local/command.py +++ b/flux_local/command.py @@ -31,6 +31,16 @@ async def run(self, stdin: bytes | None = None) -> bytes: """Execute the task and return the result.""" +def format_path(path: Path) -> str: + """Format path for debugging.""" + if path.is_absolute(): + cwd = Path.cwd() + if path.is_relative_to(cwd): + rel_path = str(path.relative_to(cwd)) + return f"{rel_path} (abs)" + return str(path) + + @dataclass class Command(Task): """An instance of a command to run.""" @@ -57,7 +67,10 @@ def string(self) -> str: def __str__(self) -> str: """Render as a debug string.""" - return f"({self.cwd}) {self.string}" + cwd: str = "" + if self.cwd: + cwd = f"({format_path(self.cwd)}) " + return f"{cwd}{self.string}" async def run(self, stdin: bytes | None = None) -> bytes: """Run the command, returning stdout.""" @@ -83,7 +96,7 @@ async def run(self, stdin: bytes | None = None) -> bytes: errors.append(out.decode("utf-8")) if err: errors.append(err.decode("utf-8")) - _LOGGER.error("\n".join(errors)) + _LOGGER.debug("\n".join(errors)) raise self.exc("\n".join(errors)) return out @@ -93,7 +106,10 @@ async def _run_piped_with_sem(cmds: Sequence[Task]) -> str: stdin = None out = None for cmd in cmds: - out = await asyncio.wait_for(cmd.run(stdin), _TIMEOUT) + try: + out = await asyncio.wait_for(cmd.run(stdin), _TIMEOUT) + except asyncio.exceptions.TimeoutError as err: + raise cmd.exc(f"Command '{cmd}' timed out") from err stdin = out return out.decode("utf-8") if out else "" diff --git a/flux_local/context.py b/flux_local/context.py new file mode 100644 index 00000000..87766389 --- /dev/null +++ b/flux_local/context.py @@ -0,0 +1,31 @@ +"""Utilities for context tracing.""" + +import contextvars +from contextlib import contextmanager +import logging +from time import perf_counter +from typing import Generator + + +_LOGGER = logging.getLogger(__name__) + +# No public API +__all__: list[str] = [] + + +trace: contextvars.ContextVar[list[str]] = contextvars.ContextVar("trace") + + +@contextmanager +def trace_context(name: str) -> Generator[None, None, None]: + stack = trace.get([]) + token = trace.set(stack + [name]) + label = " > ".join(stack + [name]) + t1 = perf_counter() + _LOGGER.debug("[Trace] > %s", label) + try: + yield + finally: + t2 = perf_counter() + trace.reset(token) + _LOGGER.debug("[Trace] < %s (%0.2fs)", label, (t2 - t1)) diff --git a/flux_local/exceptions.py b/flux_local/exceptions.py index 4b4039d4..0bdd3921 100644 --- a/flux_local/exceptions.py +++ b/flux_local/exceptions.py @@ -23,6 +23,10 @@ class KustomizeException(CommandException): """Raised when there is a failure running a kustomize command.""" +class KustomizePathException(CommandException): + """Raised a Kustomization points to a path that does not exist.""" + + class HelmException(CommandException): """Raised when there is a failure running a helm command.""" diff --git a/flux_local/git_repo.py b/flux_local/git_repo.py index 8c7bf5bf..3b115600 100644 --- a/flux_local/git_repo.py +++ b/flux_local/git_repo.py @@ -42,8 +42,8 @@ import git -from . import kustomize -from .exceptions import FluxException +from . import kustomize, helm +from .exceptions import FluxException, KustomizePathException from .manifest import ( CRD_KIND, FLUXTOMIZE_DOMAIN, @@ -54,9 +54,13 @@ HelmRepository, Kustomization, Manifest, + ConfigMap, + Secret, SECRET_KIND, + CONFIG_MAP_KIND, ) from .exceptions import InputException +from .context import trace_context __all__ = [ "build_manifest", @@ -199,7 +203,6 @@ class ResourceVisitor: func: Callable[ [ - Path, Path, Kustomization | HelmRelease | HelmRepository | ClusterPolicy, kustomize.Kustomize | None, @@ -217,6 +220,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.""" @@ -242,13 +265,11 @@ class MetadataSelector: @property def predicate( self, - ) -> Callable[ - [Kustomization | HelmRelease | Cluster | HelmRepository | ClusterPolicy], bool - ]: + ) -> Callable[[Kustomization | HelmRelease | HelmRepository | ClusterPolicy], bool]: """A predicate that selects Kustomization objects.""" def predicate( - obj: Kustomization | HelmRelease | Cluster | HelmRepository | ClusterPolicy, + obj: Kustomization | HelmRelease | HelmRepository | ClusterPolicy, ) -> bool: if not self.enabled: return False @@ -305,64 +326,36 @@ class ResourceSelector: cluster_policy: MetadataSelector = field(default_factory=MetadataSelector) """ClusterPolicy objects to return.""" + doc_visitor: DocumentVisitor | None = None + """Raw objects to visit.""" -async def get_fluxtomizations( - root: Path, - relative_path: Path, - build: bool, - sources: list[Source], -) -> list[Kustomization]: - """Find all flux Kustomizations in the specified path. - This may be called repeatedly with different paths to repeatedly collect - Kustomizations from the repo. Assumes that any flux Kustomization - for a GitRepository is pointed at this cluster, following normal conventions. - """ - cmd: kustomize.Kustomize - if build: - cmd = ( - kustomize.build(root / relative_path) - .grep(f"kind={CLUSTER_KUSTOMIZE_KIND}") - .grep(GREP_SOURCE_REF_KIND) - ) - else: - cmd = kustomize.grep( - f"kind={CLUSTER_KUSTOMIZE_KIND}", root / relative_path - ).grep(GREP_SOURCE_REF_KIND) - docs = await cmd.objects() - results = [] - for doc in filter(FLUXTOMIZE_DOMAIN_FILTER, docs): - ks = Kustomization.parse_doc(doc) - if not is_allowed_source(ks, sources): - continue - results.append(ks) - return results - - -def is_allowed_source(doc: Kustomization, sources: list[Source]) -> bool: +def is_allowed_source(sources: list[Source]) -> Callable[[Kustomization], bool]: """Return true if this Kustomization is from an allowed source.""" - if not sources: - return True - for source in sources: - if source.name == doc.source_name and ( - source.namespace is None or source.namespace == doc.source_namespace - ): + + def _filter(doc: Kustomization) -> bool: + if not sources: return True - return False + for source in sources: + if source.name == doc.source_name and ( + source.namespace is None or source.namespace == doc.source_namespace + ): + return True + return False + + return _filter -def adjust_ks_path( - doc: Kustomization, default_path: Path, sources: list[Source] -) -> Path | None: +def adjust_ks_path(doc: Kustomization, selector: PathSelector) -> Path | None: """Make adjustments to the Kustomizations path.""" # Source path is relative to the search path. Update to have the # full prefix relative to the root. if not doc.path: - _LOGGER.debug("Assigning implicit path %s", default_path) - return default_path + _LOGGER.debug("Assigning implicit path %s", selector.relative_path) + return selector.relative_path if doc.source_kind == OCI_REPO_KIND: - for source in sources: + for source in selector.sources or []: if source.name == doc.source_name: _LOGGER.debug( "Updated Source for OCIRepository %s: %s", doc.name, doc.path @@ -377,68 +370,135 @@ def adjust_ks_path( ) return None - return Path(doc.path) + path = Path(doc.path) + if path.is_absolute(): + return path.relative_to("/") + return path + + +class CachableBuilder: + """Wrappwr around flux_build that caches contents.""" + + def __init__(self) -> None: + """Initialize CachableBuilder.""" + self._cache: dict[str, kustomize.Kustomize] = {} + + async def build( + self, kustomization: Kustomization, path: Path + ) -> kustomize.Kustomize: + key = f"{kustomization.namespaced_name} @ {path}" + if cmd := self._cache.get(key): + return cmd + cmd = kustomize.flux_build(kustomization, path) + cmd = await cmd.stash() + self._cache[key] = cmd + return cmd + + +async def visit_kustomization( + selector: PathSelector, + builder: CachableBuilder, + path: Path, + visit_ks: Kustomization | None, +) -> list[Kustomization]: + """Visit a path and return a list of Kustomizations.""" + + _LOGGER.debug("Visiting path (%s) %s", selector.path, path) + label = visit_ks.namespaced_name if visit_ks else str(path) + with trace_context(f"Kustomization '{label}'"): + cmd: kustomize.Kustomize + if visit_ks is None: + cmd = kustomize.grep(f"kind={CLUSTER_KUSTOMIZE_KIND}", selector.root / path) + else: + cmd = await builder.build(visit_ks, selector.root / path) + cmd = cmd.grep(f"kind={CLUSTER_KUSTOMIZE_KIND}") + cmd = cmd.grep(GREP_SOURCE_REF_KIND) + + try: + docs = await cmd.objects() + except KustomizePathException as err: + raise FluxException(err) from err + except FluxException as err: + if visit_ks is None: + raise FluxException( + f"Error building Fluxtomization in '{selector.root}' " + f"path '{path}': {ERROR_DETAIL_BAD_PATH} {err}" + ) from err + raise FluxException( + f"Error building Fluxtomization '{visit_ks.namespaced_name}' " + f"path '{path}': {ERROR_DETAIL_BAD_KS} {err}" + ) from err + kustomizations = list( + filter( + is_allowed_source(selector.sources or []), + [ + Kustomization.parse_doc(doc) + for doc in filter(FLUXTOMIZE_DOMAIN_FILTER, docs) + ], + ) + ) + unique = {ks.namespaced_name for ks in kustomizations} + if len(unique) != len(kustomizations): + ks_names = [ks.namespaced_name for ks in kustomizations] + dupes = list(filter(lambda x: ks_names.count(x) > 1, ks_names)) + raise FluxException( + f"Detected multiple Fluxtomizations with the same name: {dupes}. " + "This indicates either (1) an incorrect Kustomization which needs to be fixed " + "or (2) a multi-cluster setup which requires flux-local to run with a more strict --path." + ) + return kustomizations async def kustomization_traversal( - root_path_selector: PathSelector, path_selector: PathSelector, build: bool + selector: PathSelector, builder: CachableBuilder ) -> list[Kustomization]: """Search for kustomizations in the specified path.""" - kustomizations: list[Kustomization] = [] + response_kustomizations: list[Kustomization] = [] visited_paths: set[Path] = set() # Relative paths within the cluster visited_ks: set[str] = set() - path_queue: queue.Queue[Path] = queue.Queue() - path_queue.put(path_selector.relative_path) + path_queue: queue.Queue[tuple[Path, Kustomization | None]] = queue.Queue() + path_queue.put((selector.relative_path, None)) while not path_queue.empty(): - path = path_queue.get() - _LOGGER.debug("Visiting path (%s) %s", root_path_selector.path, path) - try: - docs = await get_fluxtomizations( - root_path_selector.root, - path, - build=build, - sources=path_selector.sources or [], - ) - except FluxException as err: - detail = ERROR_DETAIL_BAD_KS if visited_paths else ERROR_DETAIL_BAD_PATH - raise FluxException( - f"Error building Fluxtomization in '{root_path_selector.root}' " - f"path '{path}': {err} - {detail}" - ) + # Fully empty the queue, running all tasks in parallel + tasks = [] + while not path_queue.empty(): + (path, visit_ks) = path_queue.get() + + if path in visited_paths: + _LOGGER.debug("Already visited %s", path) + continue + visited_paths.add(path) - visited_paths |= set({path}) + tasks.append(visit_kustomization(selector, builder, path, visit_ks)) - orig_len = len(docs) - docs = [doc for doc in docs if doc.namespaced_name not in visited_ks] - visited_ks |= set({doc.namespaced_name for doc in docs}) - new_len = len(docs) - _LOGGER.debug("Found %s Kustomizations (%s unique)", orig_len, new_len) + # Find new kustomizations + kustomizations = [] + for result in await asyncio.gather(*tasks): + for ks in result: + if ks.namespaced_name in visited_ks: + continue + kustomizations.append(ks) + visited_ks.add(ks.namespaced_name) + _LOGGER.debug("Found %s new Kustomizations", len(kustomizations)) - result_docs = [] - for doc in docs: + # Queue up new paths to visit to find more kustomizations + for ks in kustomizations: _LOGGER.debug( "Kustomization '%s' sourceRef.kind '%s' of '%s'", - doc.name, - doc.source_kind, - doc.source_name, + ks.name, + ks.source_kind, + ks.source_name, ) - if not ( - doc_path := adjust_ks_path( - doc, root_path_selector.relative_path, path_selector.sources or [] - ) - ): + if not (ks_path := adjust_ks_path(ks, selector)): continue - doc.path = str(doc_path) - if doc_path not in visited_paths: - path_queue.put(doc_path) - else: - _LOGGER.debug("Already visited %s", doc_path) - result_docs.append(doc) - kustomizations.extend(result_docs) - kustomizations.sort(key=lambda x: (x.namespace, x.name)) - return kustomizations + ks.path = str(ks_path) + path_queue.put((ks_path, ks)) + response_kustomizations.append(ks) + + response_kustomizations.sort(key=lambda x: (x.namespace, x.name)) + return response_kustomizations def node_name(ks: Kustomization) -> str: @@ -450,142 +510,118 @@ def node_name(ks: Kustomization) -> str: return f"{ks.namespaced_name} @ {ks.id_name}" -async def get_clusters( - path_selector: PathSelector, - cluster_selector: MetadataSelector, - kustomization_selector: MetadataSelector, -) -> list[Cluster]: - """Load Cluster objects from the specified path.""" - try: - roots = await get_fluxtomizations( - path_selector.root, - path_selector.relative_path, - build=False, - sources=path_selector.sources or [], - ) - except FluxException as err: - raise FluxException( - f"Error building Fluxtomization in '{path_selector.root}' path " - f"'{path_selector.relative_path}': {err}" - f"Try specifying another path within the git repo?" - ) - _LOGGER.debug("roots=%s", roots) - clusters = [ - Cluster(name=ks.name, namespace=ks.namespace or "", path=ks.path) - for ks in roots - if cluster_selector.predicate(ks) - ] - build = True - if not clusters: - # There are no flux-system Kustomizations within this path. Fall back to - # assuming everything in the current directory is part of a cluster. - _LOGGER.debug( - "No clusters found; Processing as a Kustomization: %s", - path_selector.relative_path, - ) - clusters = [ - Cluster(name="cluster", namespace="", path=str(path_selector.relative_path)) - ] - build = False - - tasks = [] - for cluster in clusters: - _LOGGER.debug("Building cluster %s %s", cluster.name, cluster.path) - tasks.append( - kustomization_traversal( - path_selector, - PathSelector(path=Path(cluster.path), sources=path_selector.sources), - build=build, - ) - ) - finished = await asyncio.gather(*tasks) - for cluster, results in zip(clusters, finished): - cluster.kustomizations = [ - ks for ks in results if kustomization_selector.predicate(ks) - ] - clusters.sort(key=lambda x: (x.path, x.namespace, x.name)) - return clusters - - 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], -) -> tuple[Iterable[HelmRepository], Iterable[HelmRelease], Iterable[ClusterPolicy]]: + builder: CachableBuilder, +) -> tuple[ + Iterable[HelmRepository], + Iterable[HelmRelease], + Iterable[ClusterPolicy], + Iterable[ConfigMap], + Iterable[Secret], +]: """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 ([], [], []) - cmd = kustomize.build(root / kustomization.path, kustomize_flags) - skips = [] - if kustomization_selector.skip_crds: - skips.append(CRD_KIND) - if kustomization_selector.skip_secrets: - skips.append(SECRET_KIND) - cmd = cmd.skip_resources(skips) - try: - cmd = await cmd.stash() - except FluxException as err: - raise FluxException( - f"Error while building Kustomization " - f"'{kustomization.namespace}/{kustomization.name}' " - f"(path={kustomization.source_path}): {err}" - ) from err + return ([], [], [], [], []) + + with trace_context(f"Build '{kustomization.namespaced_name}'"): + cmd = await builder.build(kustomization, root / kustomization.path) + skips = [] + if kustomization_selector.skip_crds: + skips.append(CRD_KIND) + if kustomization_selector.skip_secrets: + skips.append(SECRET_KIND) + cmd = cmd.skip_resources(skips) + try: + cmd = await cmd.stash() + except FluxException as err: + raise FluxException( + f"Error while building Kustomization " + f"'{kustomization.namespace}/{kustomization.name}' " + f"(path={kustomization.source_path}): {err}" + ) from err - if kustomization_selector.visitor: if kustomization_selector.visitor: 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 - ): - return ([], [], []) + kinds = [] + if helm_repo_selector.enabled: + kinds.append(HELM_REPO_KIND) + if helm_release_selector.enabled: + kinds.append(HELM_RELEASE_KIND) + # Needed for expanding value references + kinds.append(CONFIG_MAP_KIND) + kinds.append(SECRET_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 ([], [], [], [], []) + + regexp = f"kind=^({'|'.join(kinds)})$" + docs = await cmd.grep(regexp).objects( + target_namespace=kustomization.target_namespace + ) - docs = await cmd.grep( - f"kind=^({HELM_REPO_KIND}|{HELM_RELEASE_KIND}|{CLUSTER_POLICY_KIND})$" - ).objects() - return ( - filter( - helm_repo_selector.predicate, - [ - HelmRepository.parse_doc(doc) - for doc in docs - if doc.get("kind") == HELM_REPO_KIND - ], - ), - filter( - helm_release_selector.predicate, - [ - HelmRelease.parse_doc(doc) - for doc in docs - if doc.get("kind") == HELM_RELEASE_KIND - ], - ), - filter( - cluster_policy_selector.predicate, + if selector.doc_visitor: + doc_kinds = set(selector.doc_visitor.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, + [ + HelmRepository.parse_doc(doc) + for doc in docs + if doc.get("kind") == HELM_REPO_KIND + ], + ), + filter( + helm_release_selector.predicate, + [ + HelmRelease.parse_doc(doc) + for doc in docs + if doc.get("kind") == HELM_RELEASE_KIND + ], + ), + filter( + cluster_policy_selector.predicate, + [ + ClusterPolicy.parse_doc(doc) + for doc in docs + if doc.get("kind") == CLUSTER_POLICY_KIND + ], + ), [ - ClusterPolicy.parse_doc(doc) + ConfigMap.parse_doc(doc) for doc in docs - if doc.get("kind") == CLUSTER_POLICY_KIND + if doc.get("kind") == CONFIG_MAP_KIND ], - ), - ) + [Secret.parse_doc(doc) for doc in docs if doc.get("kind") == SECRET_KIND], + ) async def build_manifest( @@ -608,78 +644,97 @@ async def build_manifest( if not selector.cluster.enabled: return Manifest(clusters=[]) - clusters = await get_clusters( - selector.path, selector.cluster, selector.kustomization - ) + builder = CachableBuilder() - async def update_kustomization(cluster: Cluster) -> None: - build_tasks = [] - for kustomization in cluster.kustomizations: - _LOGGER.debug( - "Processing kustomization '%s': %s", - kustomization.name, - kustomization.path, - ) - build_tasks.append( - build_kustomization( - kustomization, - Path(cluster.path), - selector.path.root, - selector.kustomization, - selector.helm_release, - selector.helm_repo, - selector.cluster_policy, - options.kustomize_flags, - ) + with trace_context(f"Cluster '{str(selector.path.path)}'"): + results = await kustomization_traversal(selector.path, builder) + clusters = [ + Cluster( + path=str(selector.path.relative_path), + kustomizations=[ + ks for ks in results if selector.kustomization.predicate(ks) + ], ) - results = list(await asyncio.gather(*build_tasks)) - for kustomization, (helm_repos, helm_releases, cluster_policies) in zip( - cluster.kustomizations, - results, - ): - kustomization.helm_repos = list(helm_repos) - kustomization.helm_releases = list(helm_releases) - kustomization.cluster_policies = list(cluster_policies) - - kustomization_tasks = [] - # Expand and visit Kustomizations - for cluster in clusters: - kustomization_tasks.append(update_kustomization(cluster)) - await asyncio.gather(*kustomization_tasks) - - # Visit Helm resources - for cluster in clusters: - if selector.helm_repo.visitor: - for kustomization in cluster.kustomizations: - for helm_repo in kustomization.helm_repos: - await selector.helm_repo.visitor.func( - Path(cluster.path), - Path(kustomization.path), - helm_repo, - None, - ) + ] - if selector.helm_release.visitor: + async def update_kustomization(cluster: Cluster) -> None: + build_tasks = [] for kustomization in cluster.kustomizations: - for helm_release in kustomization.helm_releases: - await selector.helm_release.visitor.func( + _LOGGER.debug( + "Processing kustomization '%s': %s", + kustomization.name, + kustomization.path, + ) + build_tasks.append( + build_kustomization( + kustomization, Path(cluster.path), - Path(kustomization.path), - helm_release, - None, + selector, + options.kustomize_flags, + builder, ) - - if selector.cluster_policy.visitor: + ) + results = list(await asyncio.gather(*build_tasks)) + for kustomization, ( + helm_repos, + helm_releases, + cluster_policies, + config_maps, + secrets, + ) in zip( + cluster.kustomizations, + results, + ): + kustomization.helm_repos = list(helm_repos) + kustomization.helm_releases = list(helm_releases) + kustomization.cluster_policies = list(cluster_policies) + kustomization.config_maps = list(config_maps) + kustomization.secrets = list(secrets) + + kustomization_tasks = [] + # Expand and visit Kustomizations + for cluster in clusters: + kustomization_tasks.append(update_kustomization(cluster)) + await asyncio.gather(*kustomization_tasks) + + # Handle any HelmRelease value references + for cluster in clusters: for kustomization in cluster.kustomizations: - for cluster_policy in kustomization.cluster_policies: - await selector.cluster_policy.visitor.func( - Path(cluster.path), - Path(kustomization.path), - cluster_policy, - None, - ) - - return Manifest(clusters=clusters) + kustomization.helm_releases = [ + helm.expand_value_references(helm_release, kustomization) + for helm_release in kustomization.helm_releases + ] + + # Visit Helm resources + for cluster in clusters: + if selector.helm_repo.visitor: + for kustomization in cluster.kustomizations: + for helm_repo in kustomization.helm_repos: + await selector.helm_repo.visitor.func( + Path(kustomization.path), + helm_repo, + None, + ) + + if selector.helm_release.visitor: + for kustomization in cluster.kustomizations: + for helm_release in kustomization.helm_releases: + await selector.helm_release.visitor.func( + Path(kustomization.path), + helm_release, + None, + ) + + if selector.cluster_policy.visitor: + for kustomization in cluster.kustomizations: + for cluster_policy in kustomization.cluster_policies: + await selector.cluster_policy.visitor.func( + Path(kustomization.path), + cluster_policy, + None, + ) + + return Manifest(clusters=clusters) @contextlib.contextmanager diff --git a/flux_local/helm.py b/flux_local/helm.py index cf05edd1..63c15f4c 100644 --- a/flux_local/helm.py +++ b/flux_local/helm.py @@ -32,18 +32,31 @@ ``` """ +import base64 +from collections.abc import Sequence import datetime from dataclasses import dataclass import logging from pathlib import Path -from typing import Any +from typing import Any, TypeVar import aiofiles import yaml from . import command from .kustomize import Kustomize -from .manifest import HelmRelease, HelmRepository, CRD_KIND, SECRET_KIND, REPO_TYPE_OCI +from .manifest import ( + HelmRelease, + HelmRepository, + CRD_KIND, + SECRET_KIND, + REPO_TYPE_OCI, + Kustomization, + CONFIG_MAP_KIND, + ConfigMap, + Secret, + VALUE_PLACEHOLDER, +) from .exceptions import HelmException __all__ = [ @@ -163,7 +176,7 @@ def add_repos(self, repos: list[HelmRepository]) -> None: self._repos.append(repo) async def update(self) -> None: - """Return command line arguments to update the local repo. + """Run the update command to update the local repo. Typically the repository must be updated before doing any chart templating. """ @@ -185,11 +198,9 @@ async def template( release: HelmRelease, options: Options | None = None, ) -> Kustomize: - """Return command line arguments to template the specified chart. + """Return command to evaluate the template of the specified chart. - The default values will come from the `HelmRelease`, though you can - also specify values directory if not present in cluster manifest - e.g. it came from a truncated yaml. + The values will come from the `HelmRelease` object. """ if options is None: options = Options() @@ -200,7 +211,8 @@ async def template( if not repo: raise HelmException( f"Unable to find HelmRepository for {release.chart.chart_name} for " - f"HelmRelease {release.name}" + f"HelmRelease {release.name} " + f"({len(self._repos)} other HelmRepositories in --path)" ) args: list[str] = [ HELM_BIN, @@ -227,3 +239,128 @@ async def template( if options.skip_resources: cmd = cmd.skip_resources(options.skip_resources) return cmd + + +_T = TypeVar("_T", bound=ConfigMap | Secret) + + +def _find_object(name: str, namespace: str, objects: Sequence[_T]) -> _T | None: + """Find the object in the list of objects.""" + for obj in objects: + if obj.name == name and obj.namespace == namespace: + return obj + return None + + +def _decode_config_or_secret_value( + name: str, string_data: dict[str, str] | None, binary_data: dict[str, str] | None +) -> dict[str, str] | None: + """Return the config or secret data.""" + if binary_data: + try: + return { + k: base64.b64decode(v).decode("utf-8") for k, v in binary_data.items() + } + except ValueError: + raise HelmException(f"Unable to decode binary data for configmap {name}") + return string_data + + +def _get_secret_data( + name: str, namespace: str, ks: Kustomization +) -> dict[str, str] | None: + """Find the secret value in the kustomization.""" + found: Secret | None = _find_object(name, namespace, ks.secrets) + if not found: + return None + return _decode_config_or_secret_value( + f"{namespace}/{name}", found.string_data, found.data + ) + + +def _get_configmap_data( + name: str, namespace: str, ks: Kustomization +) -> dict[str, str] | None: + """Find the configmap value in the kustomization.""" + found: ConfigMap | None = _find_object(name, namespace, ks.config_maps) + if not found: + return None + return _decode_config_or_secret_value( + f"{namespace}/{name}", found.data, found.binary_data + ) + + +def expand_value_references( + helm_release: HelmRelease, kustomization: Kustomization +) -> HelmRelease: + """Expand value references in the HelmRelease.""" + if not helm_release.values_from: + return helm_release + + values = helm_release.values or {} + for ref in helm_release.values_from: + _LOGGER.debug("Expanding value reference %s", ref) + found_data: dict[str, str] | None = None + if ref.kind == SECRET_KIND: + found_data = _get_secret_data( + ref.name, helm_release.namespace, kustomization + ) + elif ref.kind == CONFIG_MAP_KIND: + found_data = _get_configmap_data( + ref.name, helm_release.namespace, kustomization + ) + else: + _LOGGER.warning( + "Unsupported valueFrom kind %s in HelmRelease %s", + ref.kind, + helm_release.namespaced_name, + ) + continue + + if found_data is None: + if not ref.optional: + _LOGGER.warning( + "Unable to find %s %s/%s referenced in HelmRelease %s", + ref.kind, + helm_release.namespace, + ref.name, + helm_release.namespaced_name, + ) + if ref.target_path: + # When a target path is specified, the value is expected to be + # a simple value type. Create a synthetic placeholder value + found_value = VALUE_PLACEHOLDER + else: + continue + elif (found_value := found_data.get(ref.values_key)) is None: + _LOGGER.warning( + "Unable to find key %s in %s/%s referenced in HelmRelease %s", + ref.values_key, + helm_release.namespace, + ref.name, + helm_release.namespaced_name, + ) + continue + + if ref.target_path: + parts = ref.target_path.split(".") + inner_values = values + for part in parts[:-1]: + if part not in inner_values: + inner_values[part] = {} + elif not isinstance(inner_values[part], dict): + raise HelmException( + f"While building HelmRelease '{helm_release.namespaced_name}': Expected '{ref.name}' field '{ref.target_path}' values to be a dict, found {type(inner_values[part])}" + ) + inner_values = inner_values[part] + + inner_values[parts[-1]] = found_value + else: + obj = yaml.load(found_value, Loader=yaml.SafeLoader) + if not obj or not isinstance(obj, dict): + raise HelmException( + f"While building HelmRelease '{helm_release.namespaced_name}': Expected '{ref.name}' field '{ref.target_path}' values to be valid yaml, found {type(values)}" + ) + values.update(obj) + + return helm_release.model_copy(update={"values": values}) diff --git a/flux_local/image.py b/flux_local/image.py new file mode 100644 index 00000000..65d647f6 --- /dev/null +++ b/flux_local/image.py @@ -0,0 +1,75 @@ +"""Helper functions for working with container images.""" + +import logging +from typing import Any + +from . import git_repo, manifest + +_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 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) + kustomization.images.sort() diff --git a/flux_local/kustomize.py b/flux_local/kustomize.py index 3eaa2c5d..7ac10af5 100644 --- a/flux_local/kustomize.py +++ b/flux_local/kustomize.py @@ -34,10 +34,7 @@ You can apply kyverno policies to the objects with the `validate` method. """ -import aiofiles -from aiofiles.os import listdir -from aiofiles.ospath import isdir, exists -import asyncio +from aiofiles.ospath import isdir import logging from pathlib import Path import tempfile @@ -46,30 +43,27 @@ import yaml from . import manifest -from .command import Command, run_piped, Task +from .command import Command, run_piped, Task, format_path from .exceptions import ( InputException, KustomizeException, KyvernoException, + KustomizePathException, ) +from .manifest import Kustomization _LOGGER = logging.getLogger(__name__) __all__ = [ - "build", + "flux_build", "grep", "Kustomize", ] KUSTOMIZE_BIN = "kustomize" KYVERNO_BIN = "kyverno" +FLUX_BIN = "flux" HELM_RELEASE_KIND = "HelmRelease" -KUSTOMIZE_FILES = ["kustomization.yaml", "kustomization.yml", "Kustomization"] - -# Use the same behavior as flux to allow loading files outside the directory -# containing kustomization.yaml -# https://fluxcd.io/flux/faq/#what-is-the-behavior-of-kustomize-used-by-flux -KUSTOMIZE_BUILD_FLAGS = ["--load-restrictor=LoadRestrictionsNone"] class Kustomize: @@ -113,15 +107,21 @@ async def run(self) -> str: """Run the kustomize command and return the output as a string.""" return await run_piped(self._cmds) - async def _docs(self) -> AsyncGenerator[dict[str, Any], None]: + async def _docs( + self, target_namespace: str | None = None + ) -> AsyncGenerator[dict[str, Any], None]: """Run the kustomize command and return the result documents.""" out = await self.run() for doc in yaml.safe_load_all(out): + if target_namespace is not None: + doc = update_namespace(doc, target_namespace) yield doc - async def objects(self) -> list[dict[str, Any]]: + async def objects( + self, target_namespace: str | None = None + ) -> list[dict[str, Any]]: """Run the kustomize command and return the result cluster objects as a list.""" - return [doc async for doc in self._docs()] + return [doc async for doc in self._docs(target_namespace=target_namespace)] def skip_resources(self, kinds: list[str]) -> "Kustomize": """Skip resources kinds of the specified types.""" @@ -189,86 +189,57 @@ async def run(self, stdin: bytes | None = None) -> bytes: return self._out -class Build(Task): - """A task that issues a build command, handling implicit Kustomizations.""" +class FluxBuild(Task): + """A task that issues a flux build command.""" - def __init__(self, path: Path, kustomize_flags: list[str] | None = None) -> None: + def __init__(self, ks: Kustomization, path: Path) -> None: """Initialize Build.""" + self._ks = ks self._path = path - self._kustomize_flags = list(KUSTOMIZE_BUILD_FLAGS) - if kustomize_flags: - self._kustomize_flags.extend(kustomize_flags) async def run(self, stdin: bytes | None = None) -> bytes: """Run the task.""" if stdin is not None: raise InputException("Invalid stdin cannot be passed to build command") - if not await isdir(self._path): - raise InputException(f"Specified path is not a directory: {self._path}") - if not await can_kustomize_dir(self._path): - # Attempt to effectively generate a kustomization.yaml on the fly - # mirroring the behavior of flux - return await fluxtomize(self._path) - - args = [KUSTOMIZE_BIN, "build"] - args.extend(self._kustomize_flags) - cwd: Path | None = None - if self._path.is_absolute(): - cwd = self._path - else: - args.append(str(self._path)) - task = Command(args, cwd=cwd, exc=KustomizeException) - return await task.run() - - -async def can_kustomize_dir(path: Path) -> bool: - """Return true if a kustomize file exists for the specified directory.""" - for name in KUSTOMIZE_FILES: - if await exists(path / name): - return True - return False - - -async def yaml_load_all(path: Path) -> list[dict[str, Any]]: - """Load all documents from the file.""" - async with aiofiles.open(path) as f: - contents = await f.read() - return list(yaml.load_all(contents, Loader=yaml.Loader)) - - -async def fluxtomize(path: Path) -> bytes: - """Create a synthentic Kustomization file and attempt to build it. - - This is similar to the behavior of flux, which kustomize does not support - directly. Every yaml file found is read and written to the output. Every - directory that can be kustomized is built using the CLI like build() - """ - # Every file resource is read and output - # Every directory is kustomized - tasks = [] - filenames = list(await listdir(path)) - filenames.sort() - for filename in filenames: - new_path = path / filename - if new_path.is_dir(): - tasks.append(build(new_path).objects()) - elif filename.endswith(".yaml") or filename.endswith(".yml"): - tasks.append(yaml_load_all(new_path)) - else: - continue - - results = await asyncio.gather(*tasks) - docs = [] - for result in results: - docs.extend(result) - out = yaml.dump_all(docs, sort_keys=False, explicit_start=True) - return str(out).encode("utf-8") - - -def build(path: Path, kustomize_flags: list[str] | None = None) -> Kustomize: + raise KustomizePathException( + f"Kustomization '{self._ks.namespaced_name}' path field '{self._ks.path or ''}' is not a directory: {self._path}" + ) + + args = [ + FLUX_BIN, + "build", + "ks", + self._ks.name, + "--dry-run", + "--kustomization-file", + "/dev/stdin", + "--path", + str(self._path), + ] + if self._ks.namespace: + args.extend( + [ + "--namespace", + self._ks.namespace, + ] + ) + kustomization_data = yaml.dump_all( + [self._ks.contents or {}], sort_keys=False, explicit_start=True + ) + input_ks = str(kustomization_data).encode("utf-8") + + task = Command(args, cwd=None, exc=KustomizeException) + return await task.run(stdin=input_ks) + + def __str__(self) -> str: + """Render as a debug string.""" + return f"flux build {format_path(self._path)}" + + +def flux_build(ks: Kustomization, path: Path) -> Kustomize: """Build cluster artifacts from the specified path.""" - return Kustomize(cmds=[Build(path, kustomize_flags)]) + return Kustomize(cmds=[FluxBuild(ks, path)]) def grep(expr: str, path: Path, invert: bool = False) -> Kustomize: @@ -283,3 +254,13 @@ def grep(expr: str, path: Path, invert: bool = False) -> Kustomize: else: args.append(str(path)) return Kustomize([Command(args, cwd=cwd, exc=KustomizeException)]) + + +def update_namespace(doc: dict[str, Any], namespace: str) -> dict[str, Any]: + """Update the namespace of the specified document. + + Will only update the namespace if the doc appears to have a metadata/name. + """ + if (metadata := doc.get("metadata")) is not None and "name" in metadata: + doc["metadata"]["namespace"] = namespace + return doc diff --git a/flux_local/manifest.py b/flux_local/manifest.py index 67ca1691..2d4a359a 100644 --- a/flux_local/manifest.py +++ b/flux_local/manifest.py @@ -5,16 +5,14 @@ e.g. such as writing management plan for resources. """ +import base64 from pathlib import Path from typing import Any, Optional, cast import aiofiles import yaml -try: - from pydantic.v1 import BaseModel, Field -except ImportError: - from pydantic import BaseModel, Field # type: ignore +from pydantic import BaseModel, Field from .exceptions import InputException @@ -39,6 +37,10 @@ CLUSTER_POLICY_DOMAIN = "kyverno.io" CRD_KIND = "CustomResourceDefinition" SECRET_KIND = "Secret" +CONFIG_MAP_KIND = "ConfigMap" +DEFAULT_NAMESPACE = "flux-system" +VALUE_PLACEHOLDER = "**PLACEHOLDER**" +VALUE_B64_PLACEHOLDER = base64.b64encode(VALUE_PLACEHOLDER.encode()) REPO_TYPE_DEFAULT = "default" REPO_TYPE_OCI = "oci" @@ -55,8 +57,6 @@ def _check_version(doc: dict[str, Any], version: str) -> None: class BaseManifest(BaseModel): """Base class for all manifest objects.""" - _COMPACT_EXCLUDE_FIELDS: dict[str, Any] = {} - def compact_dict(self, exclude: dict[str, Any] | None = None) -> dict[str, Any]: """Return a compact dictionary representation of the object. @@ -64,14 +64,19 @@ def compact_dict(self, exclude: dict[str, Any] | None = None) -> dict[str, Any]: with variable fields removed. """ if exclude is None: - exclude = self._COMPACT_EXCLUDE_FIELDS - return self.dict(exclude=exclude) # type: ignore[arg-type] + exclude = self.compact_exclude_fields() + return self.model_dump(exclude=exclude, exclude_unset=True, exclude_none=True) + + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return {} @classmethod def parse_yaml(cls, content: str) -> "BaseManifest": """Parse a serialized manifest.""" doc = next(yaml.load_all(content, Loader=yaml.Loader), None) - return cls.parse_obj(doc) + return cls.model_validate(doc) def yaml(self, exclude: dict[str, Any] | None = None) -> str: """Return a YAML string representation of compact_dict.""" @@ -89,7 +94,7 @@ class HelmChart(BaseManifest): """The version of the chart.""" repo_name: str - """The name of the HelmRepository.""" + """The short name of the HelmRepository.""" repo_namespace: str """The namespace of the HelmRepository.""" @@ -120,12 +125,39 @@ def parse_doc(cls, doc: dict[str, Any], default_namespace: str) -> "HelmChart": repo_namespace=source_ref.get("namespace", default_namespace), ) + @property + def repo_full_name(self) -> str: + """Identifier for the HelmRepository.""" + return f"{self.repo_namespace}-{self.repo_name}" + @property def chart_name(self) -> str: """Identifier for the HelmChart.""" - return f"{self.repo_namespace}-{self.repo_name}/{self.name}" + return f"{self.repo_full_name}/{self.name}" - _COMPACT_EXCLUDE_FIELDS = {"version": True} + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return {"version": True} + + +class ValuesReference(BaseManifest): + """A reference to a resource containing values for a HelmRelease.""" + + kind: str + """The kind of resource.""" + + name: str + """The name of the resource.""" + + values_key: str = Field(alias="valuesKey", default="values.yaml") + """The key in the resource that contains the values.""" + + target_path: Optional[str] = Field(alias="targetPath", default=None) + """The path in the HelmRelease values to store the values.""" + + optional: bool = False + """Whether the reference is optional.""" class HelmRelease(BaseManifest): @@ -143,6 +175,12 @@ class HelmRelease(BaseManifest): values: Optional[dict[str, Any]] = None """The values to install in the chart.""" + values_from: Optional[list[ValuesReference]] + """A list of values to reference from an ConfigMap or Secret.""" + + images: list[str] = Field(default_factory=list) + """The list of images referenced in the HelmRelease.""" + @classmethod def parse_doc(cls, doc: dict[str, Any]) -> "HelmRelease": """Parse a HelmRelease from a kubernetes resource object.""" @@ -154,11 +192,16 @@ def parse_doc(cls, doc: dict[str, Any]) -> "HelmRelease": if not (namespace := metadata.get("namespace")): raise InputException(f"Invalid {cls} missing metadata.namespace: {doc}") chart = HelmChart.parse_doc(doc, namespace) + spec = doc["spec"] + values_from: list[ValuesReference] | None = None + if values_from_dict := spec.get("valuesFrom"): + values_from = [ValuesReference(**subdoc) for subdoc in values_from_dict] return cls( name=name, namespace=namespace, chart=chart, - values=doc["spec"].get("values"), + values=spec.get("values"), + values_from=values_from, ) @property @@ -171,10 +214,19 @@ def repo_name(self) -> str: """Identifier for the HelmRepository identified in the HelmChart.""" return f"{self.chart.repo_namespace}-{self.chart.repo_name}" - _COMPACT_EXCLUDE_FIELDS = { - "values": True, - "chart": HelmChart._COMPACT_EXCLUDE_FIELDS, - } + @property + def namespaced_name(self) -> str: + """Return the namespace and name concatenated as an id.""" + return f"{self.namespace}/{self.name}" + + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "values": True, + "values_from": True, + "chart": HelmChart.compact_exclude_fields(), + } class HelmRepository(BaseManifest): @@ -244,9 +296,97 @@ def parse_doc(cls, doc: dict[str, Any]) -> "ClusterPolicy": raise InputException(f"Invalid {cls} missing spec: {doc}") return ClusterPolicy(name=name, namespace=namespace, doc=doc) - _COMPACT_EXCLUDE_FIELDS = { - "doc": True, - } + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "doc": True, + } + + +class ConfigMap(BaseManifest): + """A ConfigMap is an API object used to store data in key-value pairs.""" + + name: str + """The name of the kustomization.""" + + namespace: str | None = None + """The namespace of the kustomization.""" + + data: dict[str, Any] | None = None + """The data in the ConfigMap.""" + + binary_data: dict[str, Any] | None = None + """The binary data in the ConfigMap.""" + + @classmethod + def parse_doc(cls, doc: dict[str, Any]) -> "ConfigMap": + """Parse a config map object from a kubernetes resource.""" + _check_version(doc, "v1") + if not (metadata := doc.get("metadata")): + raise InputException(f"Invalid {cls} missing metadata: {doc}") + if not (name := metadata.get("name")): + raise InputException(f"Invalid {cls} missing metadata.name: {doc}") + namespace = metadata.get("namespace") + return ConfigMap( + name=name, + namespace=namespace, + data=doc.get("data"), + binaryData=doc.get("binaryData"), + ) + + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "data": True, + "binaryData": True, + } + + +class Secret(BaseManifest): + """A Secret contains a small amount of sensitive data.""" + + name: str + """The name of the kustomization.""" + + namespace: str | None = None + """The namespace of the kustomization.""" + + data: dict[str, Any] | None = None + """The data in the Secret.""" + + string_data: dict[str, Any] | None = None + """The string data in the Secret.""" + + @classmethod + def parse_doc(cls, doc: dict[str, Any]) -> "Secret": + """Parse a secret object from a kubernetes resource.""" + _check_version(doc, "v1") + if not (metadata := doc.get("metadata")): + raise InputException(f"Invalid {cls} missing metadata: {doc}") + if not (name := metadata.get("name")): + raise InputException(f"Invalid {cls} missing metadata.name: {doc}") + namespace = metadata.get("namespace") + # While secrets are not typically stored in the cluster, we replace with + # placeholder values anyway. + if data := doc.get("data"): + for key, value in data.items(): + data[key] = VALUE_B64_PLACEHOLDER + if string_data := doc.get("stringData"): + for key, value in string_data.items(): + string_data[key] = VALUE_PLACEHOLDER + return Secret( + name=name, namespace=namespace, data=data, string_data=string_data + ) + + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "data": True, + "stringData": True, + } class Kustomization(BaseManifest): @@ -275,6 +415,12 @@ class Kustomization(BaseManifest): cluster_policies: list[ClusterPolicy] = Field(default_factory=list) """The set of ClusterPolicies represented in this kustomization.""" + config_maps: list[ConfigMap] = Field(default_factory=list) + """The list of config maps referenced in the kustomization.""" + + secrets: list[Secret] = Field(default_factory=list) + """The list of secrets referenced in the kustomization.""" + source_path: str | None = None """Optional source path for this Kustomization, relative to the build path.""" @@ -287,6 +433,15 @@ class Kustomization(BaseManifest): source_namespace: str | None = None """The namespace of the sourceRef that provides this Kustomization.""" + target_namespace: str | None = None + """The namespace to target when performing the operation.""" + + 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.""" @@ -310,6 +465,8 @@ def parse_doc(cls, doc: dict[str, Any]) -> "Kustomization": source_kind=source_ref.get("kind"), source_name=source_ref.get("name"), source_namespace=source_ref.get("namespace", namespace), + target_namespace=spec.get("targetNamespace"), + contents=doc, ) @property @@ -318,22 +475,33 @@ def id_name(self) -> str: return f"{self.path}" @property - def namespaced_name(self, sep: str = "/") -> str: + def namespaced_name(self) -> str: """Return the namespace and name concatenated as an id.""" - return f"{self.namespace}{sep}{self.name}" - - _COMPACT_EXCLUDE_FIELDS = { - "helm_releases": { - "__all__": HelmRelease._COMPACT_EXCLUDE_FIELDS, - }, - "cluster_policies": { - "__all__": ClusterPolicy._COMPACT_EXCLUDE_FIELDS, - }, - "source_path": True, - "source_name": True, - "source_namespace": True, - "source_kind": True, - } + return f"{self.namespace}/{self.name}" + + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "helm_releases": { + "__all__": HelmRelease.compact_exclude_fields(), + }, + "cluster_policies": { + "__all__": ClusterPolicy.compact_exclude_fields(), + }, + "config_maps": { + "__all__": ConfigMap.compact_exclude_fields(), + }, + "secrets": { + "__all__": Secret.compact_exclude_fields(), + }, + "source_path": True, + "source_name": True, + "source_namespace": True, + "source_kind": True, + "target_namespace": True, + "contents": True, + } class Cluster(BaseManifest): @@ -343,39 +511,12 @@ class Cluster(BaseManifest): a repo may also contain multiple (e.g. dev an prod). """ - name: str - """The name of the cluster.""" - - namespace: str - """The namespace of the cluster.""" - path: str """The local git repo path to the Kustomization objects for the cluster.""" kustomizations: list[Kustomization] = Field(default_factory=list) """A list of flux Kustomizations for the cluster.""" - @classmethod - def parse_doc(cls, doc: dict[str, Any]) -> "Cluster": - """Parse a partial Kustomization from a kubernetes resource.""" - _check_version(doc, FLUXTOMIZE_DOMAIN) - if not (metadata := doc.get("metadata")): - raise InputException(f"Invalid {cls} missing metadata: {doc}") - if not (name := metadata.get("name")): - raise InputException(f"Invalid {cls} missing metadata.name: {doc}") - if not (namespace := metadata.get("namespace")): - raise InputException(f"Invalid {cls} missing metadata.namespace: {doc}") - if not (spec := doc.get("spec")): - raise InputException(f"Invalid {cls} missing spec: {doc}") - if not (path := spec.get("path")): - raise InputException(f"Invalid {cls} missing spec.path: {doc}") - return Cluster(name=name, namespace=namespace, path=path) - - @property - def namespaced_name(self, sep: str = "/") -> str: - """Return the namespace and name concatenated as an id.""" - return f"{self.namespace}{sep}{self.name}" - @property def id_name(self) -> str: """Identifier for the Cluster in tests.""" @@ -408,11 +549,14 @@ def cluster_policies(self) -> list[ClusterPolicy]: for policy in kustomization.cluster_policies ] - _COMPACT_EXCLUDE_FIELDS = { - "kustomizations": { - "__all__": Kustomization._COMPACT_EXCLUDE_FIELDS, + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "kustomizations": { + "__all__": Kustomization.compact_exclude_fields(), + } } - } class Manifest(BaseManifest): @@ -421,11 +565,14 @@ class Manifest(BaseManifest): clusters: list[Cluster] """A list of Clusters represented in the repo.""" - _COMPACT_EXCLUDE_FIELDS = { - "clusters": { - "__all__": Cluster._COMPACT_EXCLUDE_FIELDS, + @classmethod + def compact_exclude_fields(cls) -> dict[str, Any]: + """Return a dictionary of fields to exclude from compact_dict.""" + return { + "clusters": { + "__all__": Cluster.compact_exclude_fields(), + } } - } async def read_manifest(manifest_path: Path) -> Manifest: diff --git a/flux_local/tool/build.py b/flux_local/tool/build.py index 63dae8eb..14b9f597 100644 --- a/flux_local/tool/build.py +++ b/flux_local/tool/build.py @@ -47,6 +47,12 @@ def register( action=BooleanOptionalAction, help="Enable use of HelmRelease inflation", ) + args.add_argument( + "--output-file", + type=str, + default="/dev/stdout", + help="Output file for the results of the command", + ) # pylint: disable=duplicate-code selector.add_common_flags(args) selector.add_helm_options_flags(args) @@ -59,6 +65,7 @@ async def run( # type: ignore[no-untyped-def] enable_helm: bool, skip_crds: bool, skip_secrets: bool, + output_file: str, **kwargs, # pylint: disable=unused-argument ) -> None: """Async Action implementation.""" @@ -97,14 +104,15 @@ async def run( # type: ignore[no-untyped-def] helm_options, ) - keys = list(content.content) - keys.sort() - for key in keys: - for line in content.content[key]: - print(line) + with open(output_file, "w") as file: + keys = list(content.content) + keys.sort() + for key in keys: + for line in content.content[key]: + print(line, file=file) - keys = list(helm_content.content) - keys.sort() - for key in keys: - for line in helm_content.content[key]: - print(line) + keys = list(helm_content.content) + keys.sort() + for key in keys: + for line in helm_content.content[key]: + print(line, file=file) diff --git a/flux_local/tool/diagnostics.py b/flux_local/tool/diagnostics.py new file mode 100644 index 00000000..68f3cee3 --- /dev/null +++ b/flux_local/tool/diagnostics.py @@ -0,0 +1,79 @@ +"""Command line tool for diagnosing configuration problems in the cluster.""" + +import logging +from argparse import ArgumentParser, _SubParsersAction as SubParsersAction +from typing import cast +import os +import pathlib +import yaml + + +_LOGGER = logging.getLogger(__name__) + +FAIL = "[DIAGNOSTICS FAIL]" +OK = "[DIAGNOSTICS OK]" + + +class DiagnosticsAction: + """Flux-local diagnostics action.""" + + @classmethod + def register( + cls, subparsers: SubParsersAction # type: ignore[type-arg] + ) -> ArgumentParser: + """Register the subparser commands.""" + args = cast( + ArgumentParser, + subparsers.add_parser( + "diagnostics", + help="Print information about local flux that can diagnose issues", + description="Print information about the local cluster to aid with diagnosing problems.", + ), + ) + args.add_argument( + "--path", + help="Optional path with flux Kustomization resources (multi-cluster ok)", + type=pathlib.Path, + default=None, + nargs="?", + ) + args.set_defaults(cls=cls) + return args + + async def run( # type: ignore[no-untyped-def] + self, + **kwargs, # pylint: disable=unused-argument + ) -> None: + """Async Action implementation.""" + path = kwargs.get("path") or "." + + errors = [] + for root, dirs, files in os.walk(str(path)): + for file in files: + if not (file.endswith(".yaml") or file.endswith(".yml")): + continue + + full_path = pathlib.Path(root) / file + try: + doc = list(yaml.safe_load_all(full_path.read_text())) + except yaml.YAMLError as err: + errors.append(f"`{full_path}` failed to parse as yaml: {err}") + continue + + for subdoc in doc: + if isinstance(subdoc, dict): + continue + if isinstance(subdoc, list): + errors.append( + f"`{full_path}` expected dictionary but was list: {subdoc}" + ) + break + errors.append( + f"`{full_path}` was not a dictionary: {type(subdoc)}: {subdoc}" + ) + + if errors: + for error in errors: + print(f"{FAIL}: {error}") + else: + print(OK) diff --git a/flux_local/tool/diff.py b/flux_local/tool/diff.py index fa508bc7..acdf8d78 100644 --- a/flux_local/tool/diff.py +++ b/flux_local/tool/diff.py @@ -4,6 +4,7 @@ import functools import os from argparse import ArgumentParser, _SubParsersAction as SubParsersAction +from collections.abc import Iterable from contextlib import contextmanager from dataclasses import asdict import difflib @@ -11,7 +12,7 @@ import pathlib import shlex import tempfile -from typing import cast, Generator, Any, AsyncGenerator +from typing import cast, Generator, Any, AsyncGenerator, TypeVar import yaml @@ -27,18 +28,28 @@ _TRUNCATE = "[Diff truncated by flux-local]" +T = TypeVar("T") + + +def _unique_keys(k1: dict[T, Any], k2: dict[T, Any]) -> Iterable[T]: + """Return an ordered set.""" + return { + **{k: True for k in k1.keys()}, + **{k: True for k in k2.keys()}, + }.keys() + def perform_object_diff( a: ObjectOutput, b: ObjectOutput, n: int, limit_bytes: int ) -> Generator[str, None, None]: """Generate diffs between the two output objects.""" - for kustomization_key in set(a.content.keys()) | set(b.content.keys()): + for kustomization_key in _unique_keys(a.content, b.content): _LOGGER.debug( "Diffing results for Kustomization %s (n=%d)", kustomization_key, n ) a_resources = a.content.get(kustomization_key, {}) b_resources = b.content.get(kustomization_key, {}) - for resource_key in set(a_resources.keys()) | set(b_resources.keys()): + for resource_key in _unique_keys(a_resources, b_resources): diff_text = difflib.unified_diff( a=a_resources.get(resource_key, []), b=b_resources.get(resource_key, []), @@ -63,14 +74,14 @@ async def perform_external_diff( ) -> AsyncGenerator[str, None]: """Generate diffs between the two output objects.""" with tempfile.TemporaryDirectory() as tmpdir: - for kustomization_key in set(a.content.keys()) | set(b.content.keys()): + for kustomization_key in _unique_keys(a.content, b.content): _LOGGER.debug( "Diffing results for Kustomization %s", kustomization_key, ) a_resources = a.content.get(kustomization_key, {}) b_resources = b.content.get(kustomization_key, {}) - keys = set(a_resources.keys()) | set(b_resources.keys()) + keys = _unique_keys(a_resources, b_resources) a_file = pathlib.Path(tmpdir) / "a.yaml" a_file.write_text( @@ -115,12 +126,12 @@ def perform_yaml_diff( """Generate diffs between the two output objects.""" diffs = [] - for kustomization_key in set(a.content.keys()) | set(b.content.keys()): + 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, {}) b_resources = b.content.get(kustomization_key, {}) resource_diffs = [] - for resource_key in set(a_resources.keys()) | set(b_resources.keys()): + for resource_key in _unique_keys(a_resources, b_resources): diff_text = difflib.unified_diff( a=a_resources.get(resource_key, []), b=b_resources.get(resource_key, []), @@ -149,22 +160,19 @@ def perform_yaml_diff( yield yaml.dump(diffs, sort_keys=False, explicit_start=True, default_style=None) -def get_helm_release_diff_keys( - a: ObjectOutput, b: ObjectOutput -) -> dict[str, list[ResourceKey]]: +def get_helm_release_diff_keys(a: ObjectOutput, b: ObjectOutput) -> list[ResourceKey]: """Return HelmRelease resource keys with diffs, by cluster.""" - result: dict[str, list[ResourceKey]] = {} - for kustomization_key in set(a.content.keys()) | set(b.content.keys()): - cluster_path = kustomization_key.cluster_path + results: list[ResourceKey] = [] + for kustomization_key in _unique_keys(a.content, b.content): _LOGGER.debug("Diffing results for Kustomization %s", kustomization_key) a_resources = a.content.get(kustomization_key, {}) b_resources = b.content.get(kustomization_key, {}) - for resource_key in set(a_resources.keys()) | set(b_resources.keys()): + for resource_key in _unique_keys(a_resources, b_resources): if resource_key.kind != "HelmRelease": continue if a_resources.get(resource_key) != b_resources.get(resource_key): - result[cluster_path] = result.get(cluster_path, []) + [resource_key] - return result + results.append(resource_key) + return results def add_diff_flags(args: ArgumentParser) -> None: @@ -241,6 +249,12 @@ def register( ), ), ) + args.add_argument( + "--output-file", + type=str, + default="/dev/stdout", + help="Output file for the results of the command", + ) selector.add_ks_selector_flags(args) add_diff_flags(args) args.set_defaults(cls=cls) @@ -252,6 +266,7 @@ async def run( # type: ignore[no-untyped-def] unified: int, strip_attrs: list[str] | None, limit_bytes: int, + output_file: str, **kwargs, # pylint: disable=unused-argument ) -> None: """Async Action implementation.""" @@ -277,19 +292,22 @@ async def run( # type: ignore[no-untyped-def] return _LOGGER.debug("Diffing content") - if output == "yaml": - result = perform_yaml_diff(orig_content, content, unified, limit_bytes) - for line in result: - print(line) - elif external_diff := os.environ.get("DIFF"): - async for line in perform_external_diff( - shlex.split(external_diff), orig_content, content, limit_bytes - ): - print(line) - else: - result = perform_object_diff(orig_content, content, unified, limit_bytes) - for line in result: - print(line) + with open(output_file, "w") as file: + if output == "yaml": + result = perform_yaml_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 + ): + print(line, file=file) + else: + result = perform_object_diff( + orig_content, content, unified, limit_bytes + ) + for line in result: + print(line, file=file) class DiffHelmReleaseAction: @@ -312,6 +330,12 @@ def register( ), ), ) + args.add_argument( + "--output-file", + type=str, + default="/dev/stdout", + help="Output file for the results of the command", + ) selector.add_hr_selector_flags(args) selector.add_helm_options_flags(args) add_diff_flags(args) @@ -324,6 +348,7 @@ async def run( # type: ignore[no-untyped-def] unified: int, strip_attrs: list[str] | None, limit_bytes: int, + output_file: str, **kwargs, # pylint: disable=unused-argument ) -> None: """Async Action implementation.""" @@ -350,7 +375,8 @@ async def run( # type: ignore[no-untyped-def] ) if not helm_visitor.releases and not orig_helm_visitor.releases: - print(selector.not_found("HelmRelease", query.helm_release)) + with open(output_file, "w") as file: + print(selector.not_found("HelmRelease", query.helm_release), file=file) return # Find HelmRelease objects with diffs and prune all other HelmReleases from @@ -359,31 +385,19 @@ async def run( # type: ignore[no-untyped-def] # This avoid building unnecessary resources and churn from things like # random secret generation. diff_resource_keys = get_helm_release_diff_keys(orig_content, content) - cluster_paths = { - kustomization_key.cluster_path - for kustomization_key in set(orig_content.content.keys()) - | set(content.content.keys()) + diff_names = { + resource_key.namespaced_name for resource_key in diff_resource_keys } - for cluster_path in cluster_paths: - diff_keys = diff_resource_keys.get(cluster_path, []) - diff_names = { - f"{resource_key.namespace}/{resource_key.name}" - for resource_key in diff_keys - } - if cluster_path in helm_visitor.releases: - releases = [ - release - for release in helm_visitor.releases[cluster_path] - if f"{release.namespace}/{release.name}" in diff_names - ] - helm_visitor.releases[cluster_path] = releases - if cluster_path in orig_helm_visitor.releases: - releases = [ - release - for release in orig_helm_visitor.releases[cluster_path] - if f"{release.namespace}/{release.name}" in diff_names - ] - orig_helm_visitor.releases[cluster_path] = releases + helm_visitor.releases = [ + release + for release in helm_visitor.releases + if release.namespaced_name in diff_names + ] + orig_helm_visitor.releases = [ + release + for release in orig_helm_visitor.releases + if release.namespaced_name in diff_names + ] helm_content = ObjectOutput(strip_attrs) orig_helm_content = ObjectOutput(strip_attrs) @@ -397,21 +411,25 @@ async def run( # type: ignore[no-untyped-def] ), ) - if output == "yaml": - for line in perform_yaml_diff( - orig_helm_content, helm_content, unified, limit_bytes - ): - print(line) - elif external_diff := os.environ.get("DIFF"): - async for line in perform_external_diff( - shlex.split(external_diff), orig_helm_content, helm_content, limit_bytes - ): - print(line) - else: - for line in perform_object_diff( - orig_helm_content, helm_content, unified, limit_bytes - ): - print(line) + with open(output_file, "w") as file: + if output == "yaml": + for line in perform_yaml_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), + orig_helm_content, + helm_content, + limit_bytes, + ): + print(line, file=file) + else: + for line in perform_object_diff( + orig_helm_content, helm_content, unified, limit_bytes + ): + print(line, file=file) class DiffAction: diff --git a/flux_local/tool/flux_local.py b/flux_local/tool/flux_local.py index e9959f1c..7ab3fe99 100644 --- a/flux_local/tool/flux_local.py +++ b/flux_local/tool/flux_local.py @@ -9,7 +9,7 @@ import yaml -from . import build, diff, get, test +from . import build, diff, get, test, diagnostics from flux_local.exceptions import FluxException _LOGGER = logging.getLogger(__name__) @@ -29,6 +29,7 @@ def _make_parser() -> argparse.ArgumentParser: get.GetAction.register(subparsers) diff.DiffAction.register(subparsers) test.TestAction.register(subparsers) + diagnostics.DiagnosticsAction.register(subparsers) return parser diff --git a/flux_local/tool/format.py b/flux_local/tool/format.py index 33178a62..cf5fa705 100644 --- a/flux_local/tool/format.py +++ b/flux_local/tool/format.py @@ -2,8 +2,11 @@ from typing import Generator, Any +import sys +from typing import TextIO import yaml + PADDING = 4 @@ -47,10 +50,10 @@ def format(self, data: list[dict[str, Any]]) -> Generator[str, None, None]: for result in format_columns(cols, rows): yield result - def print(self, data: list[dict[str, Any]]) -> None: + def print(self, data: list[dict[str, Any]], file: TextIO = sys.stdout) -> None: """Output the data objects.""" for result in self.format(data): - print(result) + print(result, file=file) class YamlFormatter: @@ -63,6 +66,8 @@ def format(self, data: list[dict[str, Any]]) -> Generator[str, None, None]: ): yield line - def print(self, data: list[dict[str, Any]]) -> None: + def print(self, data: list[dict[str, Any]], file: TextIO = sys.stdout) -> None: """Format the data objects.""" - print(yaml.dump_all(data, sort_keys=False, explicit_start=True), end="") + print( + yaml.dump_all(data, sort_keys=False, explicit_start=True), end="", file=file + ) diff --git a/flux_local/tool/get.py b/flux_local/tool/get.py index 85b94818..2ecf50c6 100644 --- a/flux_local/tool/get.py +++ b/flux_local/tool/get.py @@ -1,13 +1,21 @@ """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 +import pathlib +import tempfile -from flux_local import git_repo +from flux_local import git_repo, image, helm from .format import PrintFormatter, YamlFormatter from . import selector +from .visitor import HelmVisitor, ImageOutput _LOGGER = logging.getLogger(__name__) @@ -149,6 +157,13 @@ 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( "--output", "-o", @@ -156,38 +171,78 @@ def register( default="diff", help="Output format of the command", ) + args.add_argument( + "--output-file", + type=str, + default="/dev/stdout", + help="Output file for the results of the command", + ) args.set_defaults(cls=cls) return args async def run( # type: ignore[no-untyped-def] self, output: str, + output_file: 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" + + 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.helm_release.visitor = helm_visitor.release_visitor() + manifest = await git_repo.build_manifest( selector=query, options=selector.options(**kwargs) ) if output == "yaml": - YamlFormatter().print([manifest.compact_dict()]) + if image_visitor: + image_visitor.update_manifest(manifest) + if helm_content: + with tempfile.TemporaryDirectory() as helm_cache_dir: + await helm_visitor.inflate( + pathlib.Path(helm_cache_dir), + helm_content.visitor(), + helm.Options(), + ) + helm_content.update_manifest(manifest) + + with open(output_file, "w") as file: + YamlFormatter().print([manifest.compact_dict()], file=file) return - cols = ["name", "path", "kustomizations"] - if query.cluster.namespace is None: - cols.insert(0, "namespace") + cols = ["path", "kustomizations"] results: list[dict[str, Any]] = [] for cluster in manifest.clusters: value: dict[str, Any] = cluster.dict(include=set(cols)) value["kustomizations"] = len(cluster.kustomizations) results.append(value) - if not results: - print(selector.not_found("flux cluster Kustmization", query.cluster)) - return + with open(output_file, "w") as file: + if not results: + print( + selector.not_found("flux cluster Kustmization", query.cluster), + file=file, + ) + return - PrintFormatter(cols).print(results) + PrintFormatter(cols).print(results, file=file) class GetAction: diff --git a/flux_local/tool/selector.py b/flux_local/tool/selector.py index 690ca571..26260d1f 100644 --- a/flux_local/tool/selector.py +++ b/flux_local/tool/selector.py @@ -219,6 +219,8 @@ def build_cluster_selector( # type: ignore[no-untyped-def] if kwargs.get("all_namespaces"): selector.cluster.namespace = None selector.kustomization.namespace = None + selector.kustomization.skip_crds = kwargs["skip_crds"] + selector.kustomization.skip_secrets = kwargs["skip_secrets"] return selector diff --git a/flux_local/tool/test.py b/flux_local/tool/test.py index bb719c65..358cc816 100644 --- a/flux_local/tool/test.py +++ b/flux_local/tool/test.py @@ -18,6 +18,7 @@ import pytest from flux_local import git_repo, kustomize +from flux_local.exceptions import FluxException from flux_local.helm import Helm, Options from flux_local.manifest import ( Manifest, @@ -138,11 +139,8 @@ def runtest(self) -> None: async def async_runtest(self) -> None: """Run the Kustomizations test.""" - kustomize_flags = [] - if self.test_config.options: - kustomize_flags = self.test_config.options.kustomize_flags - cmd = await kustomize.build( - Path(self.kustomization.path), kustomize_flags + cmd = await kustomize.flux_build( + self.kustomization, Path(self.kustomization.path) ).stash() await cmd.objects() await cmd.validate_policies(self.cluster.cluster_policies) @@ -205,7 +203,7 @@ def from_parent( # type: ignore[override] """The public constructor.""" item: ClusterCollector = super().from_parent( parent=parent, - name=cluster.name, + name=cluster.path, path=Path(cluster.path), nodeid=str(Path(cluster.path)), ) @@ -274,6 +272,7 @@ def __init__( self.manifest: Manifest | None = None self.test_config = test_config self.test_filter = test_filter + self.init_error: Exception | None = None def pytest_sessionstart(self, session: pytest.Session) -> None: nest_asyncio.apply() @@ -282,17 +281,25 @@ def pytest_sessionstart(self, session: pytest.Session) -> None: async def async_pytest_sessionstart(self, session: pytest.Session) -> None: """Run the Kustomizations test.""" _LOGGER.debug("async_pytest_sessionstart") - manifest = await git_repo.build_manifest( - selector=self.selector, - options=self.test_config.options, - ) + try: + manifest = await git_repo.build_manifest( + selector=self.selector, + options=self.test_config.options, + ) + except FluxException as err: + _LOGGER.error("Failed to build manifest: %s", err) + self.init_error = err + return + self.manifest = manifest _LOGGER.debug("async_pytest_sessionstart ended") def pytest_collection(self, session: pytest.Session) -> None: _LOGGER.debug("pytest_collection:%s", session) - if not self.manifest: - raise ValueError("ManifestPlugin not initialized properly") + if self.init_error or self.manifest is None: + raise pytest.UsageError( + self.init_error or "ManifestPlugin not initialized properly" + ) from self.init_error manifest_collector = ManifestCollector.from_parent( parent=session, manifest=self.manifest, @@ -414,9 +421,6 @@ async def run( # type: ignore[no-untyped-def] str(verbosity), "--no-header", "--disable-warnings", - # Disable plugins used by this library that generates warnings - "-p", - "no:pytest-golden", ] _LOGGER.debug("pytest.main: %s", pytest_args) retcode = pytest.main( diff --git a/flux_local/tool/visitor.py b/flux_local/tool/visitor.py index 9dc01f4b..43cb856a 100644 --- a/flux_local/tool/visitor.py +++ b/flux_local/tool/visitor.py @@ -9,7 +9,7 @@ import yaml from typing import Any -from flux_local import git_repo +from flux_local import git_repo, image from flux_local.helm import Helm, Options from flux_local.kustomize import Kustomize from flux_local.manifest import ( @@ -17,6 +17,7 @@ Kustomization, HelmRepository, ClusterPolicy, + Manifest, ) @@ -38,17 +39,12 @@ class ResourceKey: """Key for a Kustomization object output.""" - cluster_path: str kustomization_path: str kind: str - namespace: str + namespace: str | None name: str def __post_init__(self) -> None: - if self.cluster_path.startswith("/"): - raise AssertionError( - f"Expected cluster_path as relative: {self.cluster_path}" - ) if self.kustomization_path.startswith("/"): raise AssertionError( f"Expected kustomization_path as relative: {self.kustomization_path}" @@ -62,22 +58,18 @@ def label(self) -> str: if self.kustomization_path and self.kustomization_path != ".": parts.append(self.kustomization_path) parts.append(" ") - elif self.cluster_path: - parts.append(self.cluster_path) - parts.append(" ") parts.append(self.compact_label) return "".join(parts) @property - def compact_label(self) -> str: - parts = [] - parts.append(self.kind) - parts.append(": ") + def namespaced_name(self) -> str: if self.namespace: - parts.append(self.namespace) - parts.append("/") - parts.append(self.name) - return "".join(parts) + return f"{self.namespace}/{self.name}" + return self.name + + @property + def compact_label(self) -> str: + return f"{self.kind}: {self.namespaced_name}" class ResourceOutput(ABC): @@ -94,7 +86,6 @@ def visitor(self) -> git_repo.ResourceVisitor: @abstractmethod async def call_async( self, - cluster_path: pathlib.Path, kustomization_path: pathlib.Path, doc: ResourceType, cmd: Kustomize | None, @@ -103,16 +94,14 @@ async def call_async( def key_func( self, - cluster_path: pathlib.Path, kustomization_path: pathlib.Path, resource: ResourceType, ) -> ResourceKey: return ResourceKey( - cluster_path=str(cluster_path), kustomization_path=str(kustomization_path), kind=resource.__class__.__name__, - namespace=resource.namespace or "", - name=resource.name or "", + namespace=resource.namespace, + name=resource.name, ) @@ -125,7 +114,6 @@ def __init__(self) -> None: async def call_async( self, - cluster_path: pathlib.Path, kustomization_path: pathlib.Path, doc: ResourceType, cmd: Kustomize | None, @@ -136,7 +124,7 @@ async def call_async( lines = content.split("\n") if lines[0] != "---": lines.insert(0, "---") - self.content[self.key_func(cluster_path, kustomization_path, doc)] = lines + self.content[self.key_func(kustomization_path, doc)] = lines def strip_attrs(metadata: dict[str, Any], strip_attributes: list[str]) -> None: @@ -153,6 +141,39 @@ def strip_attrs(metadata: dict[str, Any], strip_attributes: list[str]) -> None: break +class ImageOutput(ResourceOutput): + """Resource visitor that builds outputs for objects within the kustomization.""" + + def __init__(self) -> None: + """Initialize ObjectOutput.""" + self.image_visitor = image.ImageVisitor() + self.repo_visitor = self.image_visitor.repo_visitor() + + async def call_async( + self, + kustomization_path: pathlib.Path, + doc: ResourceType, + cmd: Kustomize | None, + ) -> None: + """Visitor function invoked to build and record resource objects.""" + if cmd and isinstance(doc, HelmRelease): + objects = await cmd.objects() + for obj in objects: + if obj.get("kind") in self.repo_visitor.kinds: + self.repo_visitor.func(doc.namespaced_name, obj) + + def update_manifest(self, manifest: Manifest) -> None: + """Update the manifest with the images found in the repo.""" + for cluster in manifest.clusters: + for kustomization in cluster.kustomizations: + for helm_release in kustomization.helm_releases: + if images := self.image_visitor.images.get( + helm_release.namespaced_name + ): + helm_release.images = list(images) + helm_release.images.sort() + + class ObjectOutput(ResourceOutput): """Resource visitor that builds outputs for objects within the kustomization.""" @@ -166,7 +187,6 @@ def __init__(self, strip_attributes: list[str] | None) -> None: async def call_async( self, - cluster_path: pathlib.Path, kustomization_path: pathlib.Path, doc: ResourceType, cmd: Kustomize | None, @@ -195,7 +215,6 @@ async def call_async( strip_attrs(meta, self.strip_attributes) resource_key = ResourceKey( kind=kind, - cluster_path=str(cluster_path), kustomization_path=str(kustomization_path), namespace=metadata.get("namespace", doc.namespace), name=metadata.get("name", ""), @@ -204,13 +223,10 @@ async def call_async( lines = content.split("\n") lines.insert(0, "---") contents[resource_key] = lines - self.content[ - self.key_func(cluster_path, kustomization_path, doc) - ] = contents + self.content[self.key_func(kustomization_path, doc)] = contents async def inflate_release( - cluster_path: pathlib.Path, helm: Helm, release: HelmRelease, visitor: git_repo.ResourceVisitor, @@ -218,7 +234,7 @@ async def inflate_release( ) -> None: cmd = await helm.template(release, options) # We can ignore the Kustomiation path since we're essentially grouping by cluster - await visitor.func(cluster_path, pathlib.Path(""), release, cmd) + await visitor.func(pathlib.Path(""), release, cmd) class HelmVisitor: @@ -226,35 +242,28 @@ class HelmVisitor: def __init__(self) -> None: """Initialize KustomizationContentOutput.""" - self.repos: dict[str, list[HelmRepository]] = {} - self.releases: dict[str, list[HelmRelease]] = {} + self.repos: list[HelmRepository] = [] + self.releases: list[HelmRelease] = [] - def active_repos(self, cluster_path: str) -> list[HelmRepository]: + @property + def active_repos(self) -> list[HelmRepository]: """Return HelpRepositories referenced by a HelmRelease.""" repo_keys: set[str] = { - f"{release.chart.repo_namespace}-{release.chart.repo_name}" - for release in self.releases.get(cluster_path, []) + release.chart.repo_full_name for release in self.releases } - return [ - repo - for repo in self.repos.get(cluster_path, []) - if repo.repo_name in repo_keys - ] + return [repo for repo in self.repos if repo.repo_name in repo_keys] def repo_visitor(self) -> git_repo.ResourceVisitor: """Return a git_repo.ResourceVisitor that points to this object.""" async def add_repo( - cluster_path: pathlib.Path, kustomization_path: pathlib.Path, doc: ResourceType, cmd: Kustomize | None, ) -> None: if not isinstance(doc, HelmRepository): raise ValueError(f"Expected HelmRepository: {doc}") - self.repos[str(cluster_path)] = self.repos.get(str(cluster_path), []) + [ - doc - ] + self.repos.append(doc) return git_repo.ResourceVisitor(func=add_repo) @@ -262,16 +271,13 @@ def release_visitor(self) -> git_repo.ResourceVisitor: """Return a git_repo.ResourceVisitor that points to this object.""" async def add_release( - cluster_path: pathlib.Path, kustomization_path: pathlib.Path, doc: ResourceType, cmd: Kustomize | None, ) -> None: if not isinstance(doc, HelmRelease): raise ValueError(f"Expected HelmRelease: {doc}") - self.releases[str(cluster_path)] = self.releases.get( - str(cluster_path), [] - ) + [doc] + self.releases.append(doc) return git_repo.ResourceVisitor(func=add_release) @@ -282,43 +288,22 @@ async def inflate( options: Options, ) -> None: """Expand and notify about HelmReleases discovered.""" - cluster_paths = set(list(self.releases)) | set(list(self.repos)) - tasks = [ - self.inflate_cluster( - helm_cache_dir, - pathlib.Path(cluster_path), - visitor, - options, - ) - for cluster_path in cluster_paths - ] - _LOGGER.debug("Waiting for cluster inflation to complete") - await asyncio.gather(*tasks) - - async def inflate_cluster( - self, - helm_cache_dir: pathlib.Path, - cluster_path: pathlib.Path, - visitor: git_repo.ResourceVisitor, - options: Options, - ) -> None: - _LOGGER.debug("Inflating Helm charts in cluster %s", cluster_path) + _LOGGER.debug("Inflating Helm charts in cluster") if not self.releases: return with tempfile.TemporaryDirectory() as tmp_dir: helm = Helm(pathlib.Path(tmp_dir), helm_cache_dir) - if active_repos := self.active_repos(str(cluster_path)): + if active_repos := self.active_repos: helm.add_repos(active_repos) await helm.update() tasks = [ inflate_release( - cluster_path, helm, release, visitor, options, ) - for release in self.releases.get(str(cluster_path), []) + for release in self.releases ] - _LOGGER.debug("Waiting for tasks to inflate %s", cluster_path) + _LOGGER.debug("Waiting for inflate tasks to complete") await asyncio.gather(*tasks) diff --git a/renovate.json5 b/renovate.json5 index 0ea2c6ac..ec42befa 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -1,8 +1,10 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:base", + "regexManagers:dockerfileVersions", ], + "timezone": "America/Los_Angeles", "assignees": ["allenporter"], "packageRules": [ { @@ -10,9 +12,18 @@ "automerge": true, "automergeType": "branch", "matchUpdateTypes": ["minor", "patch"] + }, + ], + "regexManagers": [ + { + "fileMatch": [".+\\.yaml$"], + "matchStrings": [ + "# renovate: datasource=(?[a-z-]+?) depName=(?[^\\s]+?)(?: (lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: registryUrl=(?[^\\s]+?))?\\s.+?_version: (?.+?)\\s" + ], + "extractVersionTemplate": "^v?(?.*)$", } ], "pre-commit": { - "enabled": true + "enabled": true, }, } diff --git a/requirements.txt b/requirements.txt index 08c50e6e..c8d410ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,23 @@ --e . aiofiles==23.2.1 -black==23.10.0 -coverage==7.3.2 -GitPython==3.1.40 -mypy==1.6.0 +black==23.12.1 +coverage==7.4.0 +GitPython==3.1.41 +mypy==1.8.0 nest_asyncio==1.5.8 -pdoc==14.1.0 -pip==23.3 -pre-commit==3.5.0 -pydantic==2.4.2 -pytest==7.4.2 -pytest-asyncio==0.21.1 +pdoc==14.3.0 +pip==23.3.2 +pre-commit==3.6.0 +pydantic==2.5.3 +pytest==7.4.4 +pytest-asyncio==0.23.3 pytest-cov==4.1.0 -pytest-golden==0.2.2 python-slugify==8.0.1 PyYAML==6.0.1 -ruff==0.1.0 -types-aiofiles==23.2.0.0 +ruff==0.1.13 +types-aiofiles==23.2.0.20240106 types-PyYAML==6.0.12.12 -typing-extensions==4.8.0 +typing-extensions==4.9.0 types-python-slugify==8.0.0.3 -wheel==0.41.2 -yamllint==1.32.0 +wheel==0.42.0 +yamllint==1.33.0 +syrupy==4.6.0 diff --git a/script/DESIGN.md b/script/DESIGN.md new file mode 100644 index 00000000..e808c0e7 --- /dev/null +++ b/script/DESIGN.md @@ -0,0 +1,80 @@ +# flux-local v5 + +## Introduction + +In v4, the major overhaul was to move use `flux build` as the primary way to +build fluxtomizations which simplified some of the structure. Now that the +code supports a bunch of features, it seems useful to try to rewrite to match +the next set of needs. + +## Goals + +Here are some goals for v5: + +- Increase parallelism: For example, operations that need helm templating + currently don't start until all other helm operations have finished. This + may be able to happen if we can use `dependsOn` to ensure all pre-requisites + are built (e.g. a `HelmRepository` may be from another fluxtomization). +- Unnecessary kustomization building: When building a single kustomization, + we don't necessarily need to build the entire world. Stopping early seems + like a nice feature and could use `dependsOn` to understand dependencies + to ensure building. +- Shared code patterns when parsing output of helm templates and outputs of + kustomize build for stuff like image version extraction. +- Move off of pydantic v1 API since it is deprecated. +- `flux-local build` is kind of a mess? It seems like it needs to be rethought. +- Caching is somewhat brittle, and should probably include the skip crds/secrets + flags, or just remove the need overall for caching of specific commands. +- Improved unit testing may be possible if we can pull more of the code out of + CLI commands into reusable libraries. + + +## Background + +Here is some background on what happens today for the first initial step: +- Search for any kustomization with `kustomize cfg grep kind=Kustomization` in + order to bootstrap. The idea here is to support minimal configuration input, + and the first pass at searching for a fluxtomization. +- All kustomizations found are added to a queue with some amount of path correction. +- For every kustomization found, call `flux build` on it. It is currently drained + all at once and run in parallel. +- Any additional kustomization found is added to the queue and the process of + building is repeated until there are no more. +- All results from each build step above is only looking at Kustomizations and + dropping all other results (though they are cached for later) + +The second step is to then process the results of the cluster by rebuilding +all the kustomizations again. Any CRDs and Secrets are filtered out if needed. +The results from the previous steps are cached with a brittle very specific +cache. Specifically, a few types of resources are collected: `HelmRepository`, +`HelmRelease`, and `ClusterPolicy`. Additional, generic objects are collected +with an object listener that supports arbitrary doc kinds, which in practice +is used for tracking images used. Each of the objects built are associated +with the generated `Kustomization` object. + +Then there is another pass that specifically traverses `HelmRelease` and +`HelmRepository` objects that are found and calls the listeners on them to +keep track of them associated with a specific `Kustomization` for diffing. +These are asyncio functions that that are run staged together. + +Finally, a command like diff may run another traverisal to call `helm template` +on each `HelmRelease`, recording the output for diffing. The diffing all happens +staged, lining up all staged objects together when they are found on both +sides or when finished and only on one side. + +## Initial Design Ideas + +The overall design screams for an asynchronous state management system that can +allow custom graphs/nodes to be put together and wired up at different stages. +It still likely makes sense to preserve the overall specific graph of kustomizations +and helm releases, however, there may still be smaller patterns that are reusable. + +The `pytransitions` library supports an `AsyncMachine` that may be useful for +representing this problem. It may be useful to start small and try using this +for a sub-problem that is currently somewhat hairy. + +## Alternatives + +An attactive option may be to rewrite the entire thing on golang so that it can +reuse the actual flux libraries. This is worth exploring in more detail, or +possibly prototyping. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 4ae8c78c..263aaced 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = flux-local -version = 3.1.1 +version = 4.2.2 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 @@ -20,7 +20,7 @@ python_requires = >=3.10 install_requires = aiofiles>=22.1.0 nest_asyncio>=1.5.6 - pydantic>=1.10.4 + pydantic>=2.5.2 python-slugify>=8.0.0 GitPython>=3.1.30 PyYAML>=6.0 diff --git a/tests/__snapshots__/test_git_repo.ambr b/tests/__snapshots__/test_git_repo.ambr new file mode 100644 index 00000000..a0ba9147 --- /dev/null +++ b/tests/__snapshots__/test_git_repo.ambr @@ -0,0 +1,1113 @@ +# serializer version: 1 +# name: test_build_manifest + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + dict({ + 'name': 'podinfo-config', + 'namespace': 'podinfo', + }), + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'podinfo', + 'repo_name': 'podinfo', + 'repo_namespace': 'flux-system', + }), + 'name': 'podinfo', + 'namespace': 'podinfo', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'apps', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/apps/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'flux-system', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/clusters/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + dict({ + 'name': 'test-allow-policy', + }), + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + dict({ + 'name': 'bitnami', + 'namespace': 'flux-system', + 'repo_type': 'default', + 'url': 'https://charts.bitnami.com/bitnami', + }), + dict({ + 'name': 'podinfo', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/stefanprodan/charts', + }), + dict({ + 'name': 'weave-charts', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/weaveworks/charts', + }), + ]), + 'name': 'infra-configs', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/configs', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'metallb', + 'repo_name': 'bitnami', + 'repo_namespace': 'flux-system', + }), + 'name': 'metallb', + 'namespace': 'metallb', + }), + dict({ + 'chart': dict({ + 'name': 'weave-gitops', + 'repo_name': 'weave-charts', + 'repo_namespace': 'flux-system', + }), + 'name': 'weave-gitops', + 'namespace': 'flux-system', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-controllers', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/controllers', + 'secrets': list([ + ]), + }), + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_cluster_selector_disabled + dict({ + 'clusters': list([ + ]), + }) +# --- +# name: test_helm_release_selector_disabled + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'apps', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/apps/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'flux-system', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/clusters/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + dict({ + 'name': 'test-allow-policy', + }), + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + dict({ + 'name': 'bitnami', + 'namespace': 'flux-system', + 'repo_type': 'default', + 'url': 'https://charts.bitnami.com/bitnami', + }), + dict({ + 'name': 'podinfo', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/stefanprodan/charts', + }), + dict({ + 'name': 'weave-charts', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/weaveworks/charts', + }), + ]), + 'name': 'infra-configs', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/configs', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-controllers', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/controllers', + 'secrets': list([ + ]), + }), + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_helm_release_visitor + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + dict({ + 'name': 'podinfo-config', + 'namespace': 'podinfo', + }), + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'podinfo', + 'repo_name': 'podinfo', + 'repo_namespace': 'flux-system', + }), + 'name': 'podinfo', + 'namespace': 'podinfo', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'apps', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/apps/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'flux-system', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/clusters/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + dict({ + 'name': 'test-allow-policy', + }), + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + dict({ + 'name': 'bitnami', + 'namespace': 'flux-system', + 'repo_type': 'default', + 'url': 'https://charts.bitnami.com/bitnami', + }), + dict({ + 'name': 'podinfo', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/stefanprodan/charts', + }), + dict({ + 'name': 'weave-charts', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/weaveworks/charts', + }), + ]), + 'name': 'infra-configs', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/configs', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'metallb', + 'repo_name': 'bitnami', + 'repo_namespace': 'flux-system', + }), + 'name': 'metallb', + 'namespace': 'metallb', + }), + dict({ + 'chart': dict({ + 'name': 'weave-gitops', + 'repo_name': 'weave-charts', + 'repo_namespace': 'flux-system', + }), + 'name': 'weave-gitops', + 'namespace': 'flux-system', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-controllers', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/controllers', + 'secrets': list([ + ]), + }), + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_helm_release_visitor.1 + list([ + tuple( + 'tests/testdata/cluster/apps/prod', + 'podinfo', + 'podinfo', + ), + tuple( + 'tests/testdata/cluster/infrastructure/controllers', + 'flux-system', + 'weave-gitops', + ), + tuple( + 'tests/testdata/cluster/infrastructure/controllers', + 'metallb', + 'metallb', + ), + ]) +# --- +# name: test_helm_repo_selector_disabled + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + dict({ + 'name': 'podinfo-config', + 'namespace': 'podinfo', + }), + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'podinfo', + 'repo_name': 'podinfo', + 'repo_namespace': 'flux-system', + }), + 'name': 'podinfo', + 'namespace': 'podinfo', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'apps', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/apps/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'flux-system', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/clusters/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + dict({ + 'name': 'test-allow-policy', + }), + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-configs', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/configs', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'metallb', + 'repo_name': 'bitnami', + 'repo_namespace': 'flux-system', + }), + 'name': 'metallb', + 'namespace': 'metallb', + }), + dict({ + 'chart': dict({ + 'name': 'weave-gitops', + 'repo_name': 'weave-charts', + 'repo_namespace': 'flux-system', + }), + 'name': 'weave-gitops', + 'namespace': 'flux-system', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-controllers', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/controllers', + 'secrets': list([ + ]), + }), + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_helm_repo_visitor + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + dict({ + 'name': 'podinfo-config', + 'namespace': 'podinfo', + }), + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'podinfo', + 'repo_name': 'podinfo', + 'repo_namespace': 'flux-system', + }), + 'name': 'podinfo', + 'namespace': 'podinfo', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'apps', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/apps/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'flux-system', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/clusters/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + dict({ + 'name': 'test-allow-policy', + }), + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + dict({ + 'name': 'bitnami', + 'namespace': 'flux-system', + 'repo_type': 'default', + 'url': 'https://charts.bitnami.com/bitnami', + }), + dict({ + 'name': 'podinfo', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/stefanprodan/charts', + }), + dict({ + 'name': 'weave-charts', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/weaveworks/charts', + }), + ]), + 'name': 'infra-configs', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/configs', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'metallb', + 'repo_name': 'bitnami', + 'repo_namespace': 'flux-system', + }), + 'name': 'metallb', + 'namespace': 'metallb', + }), + dict({ + 'chart': dict({ + 'name': 'weave-gitops', + 'repo_name': 'weave-charts', + 'repo_namespace': 'flux-system', + }), + 'name': 'weave-gitops', + 'namespace': 'flux-system', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-controllers', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/controllers', + 'secrets': list([ + ]), + }), + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_helm_repo_visitor.1 + list([ + tuple( + 'tests/testdata/cluster/infrastructure/configs', + 'flux-system', + 'bitnami', + ), + tuple( + 'tests/testdata/cluster/infrastructure/configs', + 'flux-system', + 'podinfo', + ), + tuple( + 'tests/testdata/cluster/infrastructure/configs', + 'flux-system', + 'weave-charts', + ), + ]) +# --- +# name: test_internal_commands[cluster2] + dict({ + "Cluster 'tests/testdata/cluster2'": dict({ + "Build 'flux-system/cluster'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/cluster-apps'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/cluster-apps-ingress-nginx'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/cluster-apps-ingress-nginx-certificates'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/cluster-apps-kubernetes-dashboard'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/cluster'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster2/flux (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/cluster-apps'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster2/apps (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/cluster-apps-ingress-nginx'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster2/apps/networking/ingress-nginx/app (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/cluster-apps-ingress-nginx-certificates'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster2/apps/networking/ingress-nginx/certificates (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/cluster-apps-kubernetes-dashboard'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster2/apps/monitoring/kubernetes-dashboard/app (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster2'": dict({ + 'cmds': list([ + '(tests/testdata/cluster2 (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_internal_commands[cluster3] + dict({ + "Cluster 'tests/testdata/cluster3'": dict({ + "Build 'flux-system/namespaces'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/tenants'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/namespaces'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster3/namespaces/overlays/cluster3 (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/tenants'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster3/tenants/overlays/cluster3 (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster3'": dict({ + 'cmds': list([ + '(tests/testdata/cluster3 (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_internal_commands[cluster4] + dict({ + "Cluster 'tests/testdata/cluster4'": dict({ + "Build 'flux-system/cluster'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/cluster-apps'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/cluster-apps-kubernetes-dashboard'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/cluster'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster4/flux (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/cluster-apps'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster4/apps (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/cluster-apps-kubernetes-dashboard'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster4/apps/monitoring/kubernetes-dashboard (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster4'": dict({ + 'cmds': list([ + '(tests/testdata/cluster4 (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_internal_commands[cluster5] + dict({ + "Cluster 'tests/testdata/cluster5'": dict({ + "Build 'flux-system/flux-system'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/flux-system'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster5/clusters/prod (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster5'": dict({ + 'cmds': list([ + '(tests/testdata/cluster5 (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_internal_commands[cluster6] + dict({ + "Cluster 'tests/testdata/cluster6'": dict({ + "Build 'flux-system/apps'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/flux-system'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/apps'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster6/apps (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/flux-system'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster6/cluster (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster6'": dict({ + 'cmds': list([ + '(tests/testdata/cluster6 (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_internal_commands[cluster7] + dict({ + "Cluster 'tests/testdata/cluster7'": dict({ + "Build 'flux-system/apps'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/charts'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/flux-system'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/apps'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster7/flux/apps (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/charts'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster7/flux/charts (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/flux-system'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster7/clusters/home (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster7'": dict({ + 'cmds': list([ + '(tests/testdata/cluster7 (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_internal_commands[cluster] + dict({ + "Cluster 'tests/testdata/cluster'": dict({ + "Build 'flux-system/apps'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/flux-system'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/infra-configs'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Build 'flux-system/infra-controllers'": dict({ + 'cmds': list([ + "kustomize cfg grep 'kind=^(CustomResourceDefinition|Secret)$' --invert-match", + "kustomize cfg grep 'kind=^(HelmRepository|HelmRelease|ConfigMap|Secret|ClusterPolicy)$'", + ]), + }), + "Kustomization 'flux-system/apps'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster/apps/prod (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/flux-system'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster/clusters/prod (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/infra-configs'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster/infrastructure/configs (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'flux-system/infra-controllers'": dict({ + 'cmds': list([ + 'flux build tests/testdata/cluster/infrastructure/controllers (abs)', + 'kustomize cfg grep kind=Kustomization', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + "Kustomization 'tests/testdata/cluster'": dict({ + 'cmds': list([ + '(tests/testdata/cluster (abs)) kustomize cfg grep kind=Kustomization .', + "kustomize cfg grep 'spec.sourceRef.kind=GitRepository|OCIRepository'", + ]), + }), + }), + }) +# --- +# name: test_kustomization_selector_disabled + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_kustomization_visitor + dict({ + 'clusters': list([ + dict({ + 'kustomizations': list([ + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + dict({ + 'name': 'podinfo-config', + 'namespace': 'podinfo', + }), + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'podinfo', + 'repo_name': 'podinfo', + 'repo_namespace': 'flux-system', + }), + 'name': 'podinfo', + 'namespace': 'podinfo', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'apps', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/apps/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + ]), + 'name': 'flux-system', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/clusters/prod', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + dict({ + 'name': 'test-allow-policy', + }), + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + ]), + 'helm_repos': list([ + dict({ + 'name': 'bitnami', + 'namespace': 'flux-system', + 'repo_type': 'default', + 'url': 'https://charts.bitnami.com/bitnami', + }), + dict({ + 'name': 'podinfo', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/stefanprodan/charts', + }), + dict({ + 'name': 'weave-charts', + 'namespace': 'flux-system', + 'repo_type': 'oci', + 'url': 'oci://ghcr.io/weaveworks/charts', + }), + ]), + 'name': 'infra-configs', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/configs', + 'secrets': list([ + ]), + }), + dict({ + 'cluster_policies': list([ + ]), + 'config_maps': list([ + ]), + 'helm_releases': list([ + dict({ + 'chart': dict({ + 'name': 'metallb', + 'repo_name': 'bitnami', + 'repo_namespace': 'flux-system', + }), + 'name': 'metallb', + 'namespace': 'metallb', + }), + dict({ + 'chart': dict({ + 'name': 'weave-gitops', + 'repo_name': 'weave-charts', + 'repo_namespace': 'flux-system', + }), + 'name': 'weave-gitops', + 'namespace': 'flux-system', + }), + ]), + 'helm_repos': list([ + ]), + 'name': 'infra-controllers', + 'namespace': 'flux-system', + 'path': 'tests/testdata/cluster/infrastructure/controllers', + 'secrets': list([ + ]), + }), + ]), + 'path': 'tests/testdata/cluster', + }), + ]), + }) +# --- +# name: test_kustomization_visitor.1 + list([ + tuple( + 'tests/testdata/cluster/apps/prod', + 'flux-system', + 'apps', + ), + tuple( + 'tests/testdata/cluster/clusters/prod', + 'flux-system', + 'flux-system', + ), + tuple( + 'tests/testdata/cluster/infrastructure/configs', + 'flux-system', + 'infra-configs', + ), + tuple( + 'tests/testdata/cluster/infrastructure/controllers', + 'flux-system', + 'infra-controllers', + ), + ]) +# --- diff --git a/tests/__snapshots__/test_helm.ambr b/tests/__snapshots__/test_helm.ambr new file mode 100644 index 00000000..ec2e1385 --- /dev/null +++ b/tests/__snapshots__/test_helm.ambr @@ -0,0 +1,28 @@ +# serializer version: 1 +# name: test_value_references + dict({ + 'ingress': dict({ + 'className': 'nginx', + 'enabled': True, + 'hosts': list([ + dict({ + 'host': 'podinfo.production', + 'paths': list([ + dict({ + 'path': '/', + 'pathType': 'ImplementationSpecific', + }), + ]), + }), + ]), + }), + 'redis': dict({ + 'enabled': True, + 'repository': 'public.ecr.aws/docker/library/redis', + 'tag': '7.0.6', + }), + 'tls': dict({ + 'crt': '**PLACEHOLDER**', + }), + }) +# --- diff --git a/tests/__snapshots__/test_kustomize.ambr b/tests/__snapshots__/test_kustomize.ambr new file mode 100644 index 00000000..661660d5 --- /dev/null +++ b/tests/__snapshots__/test_kustomize.ambr @@ -0,0 +1,81 @@ +# serializer version: 1 +# name: test_grep[path0] + ''' + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: flux-system + name: cluster-settings + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'cluster-settings.yaml' + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: 'cluster-settings.yaml' + data: + CLUSTER: dev + DOMAIN: example.org + + ''' +# --- +# name: test_grep[path1] + ''' + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: flux-system + name: cluster-settings + annotations: + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'cluster-settings.yaml' + internal.config.kubernetes.io/index: '0' + internal.config.kubernetes.io/path: 'cluster-settings.yaml' + data: + CLUSTER: dev + DOMAIN: example.org + + ''' +# --- +# name: test_objects[path0] + list([ + dict({ + 'apiVersion': 'v1', + 'data': dict({ + 'CLUSTER': 'dev', + 'DOMAIN': 'example.org', + }), + 'kind': 'ConfigMap', + 'metadata': dict({ + 'annotations': dict({ + 'config.kubernetes.io/index': '0', + 'config.kubernetes.io/path': 'cluster-settings.yaml', + 'internal.config.kubernetes.io/index': '0', + 'internal.config.kubernetes.io/path': 'cluster-settings.yaml', + }), + 'name': 'cluster-settings', + 'namespace': 'flux-system', + }), + }), + ]) +# --- +# name: test_objects[path1] + list([ + dict({ + 'apiVersion': 'v1', + 'data': dict({ + 'CLUSTER': 'dev', + 'DOMAIN': 'example.org', + }), + 'kind': 'ConfigMap', + 'metadata': dict({ + 'annotations': dict({ + 'config.kubernetes.io/index': '0', + 'config.kubernetes.io/path': 'cluster-settings.yaml', + 'internal.config.kubernetes.io/index': '0', + 'internal.config.kubernetes.io/path': 'cluster-settings.yaml', + }), + 'name': 'cluster-settings', + 'namespace': 'flux-system', + }), + }), + ]) +# --- diff --git a/tests/test_git_repo.py b/tests/test_git_repo.py index 7f401fc1..ee0128f1 100644 --- a/tests/test_git_repo.py +++ b/tests/test_git_repo.py @@ -1,41 +1,40 @@ """Tests for git_repo.""" -from pathlib import Path +from collections.abc import Sequence import io +from pathlib import Path from typing import Any -import pytest from unittest.mock import patch +import pytest +from syrupy.assertion import SnapshotAssertion + from flux_local.git_repo import ( build_manifest, ResourceSelector, ResourceVisitor, - kustomization_traversal, Source, PathSelector, is_allowed_source, + adjust_ks_path, ) -from flux_local.kustomize import Kustomize +from flux_local.kustomize import Kustomize, Stash +from flux_local.command import Task, run_piped from flux_local.manifest import Kustomization +from flux_local.context import trace TESTDATA = Path("tests/testdata/cluster") +TESTDATA_FULL_PATH = Path.cwd() / TESTDATA -async def test_build_manifest() -> None: +async def test_build_manifest(snapshot: SnapshotAssertion) -> None: """Tests for building the manifest.""" manifest = await build_manifest(TESTDATA) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 4 - assert len(cluster.helm_repos) == 3 - assert len(cluster.helm_releases) == 3 + assert manifest.compact_dict() == snapshot -async def test_cluster_selector_disabled() -> None: +async def test_cluster_selector_disabled(snapshot: SnapshotAssertion) -> None: """Tests for building the manifest.""" query = ResourceSelector() @@ -43,10 +42,10 @@ async def test_cluster_selector_disabled() -> None: query.cluster.enabled = False manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 0 + assert manifest.compact_dict() == snapshot -async def test_kustomization_selector_disabled() -> None: +async def test_kustomization_selector_disabled(snapshot: SnapshotAssertion) -> None: """Tests for building the manifest.""" query = ResourceSelector() @@ -54,15 +53,10 @@ async def test_kustomization_selector_disabled() -> None: query.kustomization.enabled = False manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 0 + assert manifest.compact_dict() == snapshot -async def test_helm_release_selector_disabled() -> None: +async def test_helm_release_selector_disabled(snapshot: SnapshotAssertion) -> None: """Tests for building the manifest with helm releases disabled.""" query = ResourceSelector() @@ -70,17 +64,10 @@ async def test_helm_release_selector_disabled() -> None: query.helm_release.enabled = False manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 4 - assert len(cluster.helm_repos) == 3 - assert len(cluster.helm_releases) == 0 + assert manifest.compact_dict() == snapshot -async def test_helm_repo_selector_disabled() -> None: +async def test_helm_repo_selector_disabled(snapshot: SnapshotAssertion) -> None: """Tests for building the manifest with helm repos disabled.""" query = ResourceSelector() @@ -88,17 +75,10 @@ async def test_helm_repo_selector_disabled() -> None: query.helm_repo.enabled = False manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 4 - assert len(cluster.helm_repos) == 0 - assert len(cluster.helm_releases) == 3 + assert manifest.compact_dict() == snapshot -async def test_kustomization_visitor() -> None: +async def test_kustomization_visitor(snapshot: SnapshotAssertion) -> None: """Tests for visiting Kustomizations.""" query = ResourceSelector() @@ -107,52 +87,18 @@ async def test_kustomization_visitor() -> None: stream = io.StringIO() visits: list[tuple[str, str, str, str]] = [] - async def write(w: Path, x: Path, y: Any, cmd: Kustomize | None) -> None: - visits.append((str(w), str(x), y.namespace, y.name)) + async def write(x: Path, y: Any, cmd: Kustomize | None) -> None: + visits.append((str(x), y.namespace, y.name)) if cmd: stream.write(await cmd.run()) query.kustomization.visitor = ResourceVisitor(func=write) manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 4 - kustomization = cluster.kustomizations[0] - assert kustomization.name == "apps" - assert kustomization.namespace == "flux-system" - assert kustomization.path == "tests/testdata/cluster/apps/prod" + assert manifest.compact_dict() == snapshot visits.sort() - assert visits == [ - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/apps/prod", - "flux-system", - "apps", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/clusters/prod", - "flux-system", - "flux-system", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/configs", - "flux-system", - "infra-configs", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/controllers", - "flux-system", - "infra-controllers", - ), - ] + assert visits == snapshot content = stream.getvalue() assert content @@ -160,7 +106,7 @@ async def write(w: Path, x: Path, y: Any, cmd: Kustomize | None) -> None: assert "name: metallb" in content -async def test_helm_repo_visitor() -> None: +async def test_helm_repo_visitor(snapshot: SnapshotAssertion) -> None: """Tests for visiting a HelmRepository objects.""" query = ResourceSelector() @@ -168,47 +114,19 @@ async def test_helm_repo_visitor() -> None: visits: list[tuple[str, str, str, str]] = [] - async def append(w: Path, x: Path, y: Any, z: Any) -> None: - visits.append((str(w), str(x), y.namespace, y.name)) + async def append(x: Path, y: Any, z: Any) -> None: + visits.append((str(x), y.namespace, y.name)) - query.helm_repo.visitor = ResourceVisitor( - func=append, - ) + query.helm_repo.visitor = ResourceVisitor(func=append) manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 4 - assert len(cluster.helm_repos) == 3 - assert len(cluster.helm_releases) == 3 + assert manifest.compact_dict() == snapshot visits.sort() - assert visits == [ - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/configs", - "flux-system", - "bitnami", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/configs", - "flux-system", - "podinfo", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/configs", - "flux-system", - "weave-charts", - ), - ] + assert visits == snapshot -async def test_helm_release_visitor() -> None: +async def test_helm_release_visitor(snapshot: SnapshotAssertion) -> None: """Tests for visiting a HelmRelease objects.""" query = ResourceSelector() @@ -216,122 +134,18 @@ async def test_helm_release_visitor() -> None: visits: list[tuple[str, str, str, str]] = [] - async def append(w: Path, x: Path, y: Any, z: Any) -> None: - visits.append((str(w), str(x), y.namespace, y.name)) + async def append(x: Path, y: Any, z: Any) -> None: + visits.append((str(x), y.namespace, y.name)) query.helm_release.visitor = ResourceVisitor( func=append, ) manifest = await build_manifest(selector=query) - assert len(manifest.clusters) == 1 - cluster = manifest.clusters[0] - assert cluster.name == "flux-system" - assert cluster.namespace == "flux-system" - assert cluster.path == "./tests/testdata/cluster/clusters/prod" - assert len(cluster.kustomizations) == 4 - assert len(cluster.helm_repos) == 3 - assert len(cluster.helm_releases) == 3 + assert manifest.compact_dict() == snapshot visits.sort() - assert visits == [ - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/apps/prod", - "podinfo", - "podinfo", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/controllers", - "flux-system", - "weave-gitops", - ), - ( - "tests/testdata/cluster/clusters/prod", - "tests/testdata/cluster/infrastructure/controllers", - "metallb", - "metallb", - ), - ] - - -@pytest.mark.parametrize( - "path", - [ - "kubernetes/flux/", - "./kubernetes/flux", - "kubernetes/flux", - ], -) -async def test_kustomization_traversal(path: str) -> None: - """Tests for finding Kustomizations.""" - - results: list[list[Kustomization]] = [ - # First traversal - [ - Kustomization( - name="flux-system", - namespace="flux-system", - path="./kubernetes/cluster", - ), - ], - # Second traversal - [ - Kustomization( - name="cluster-apps", - namespace="flux-system", - path="./kubernetes/apps", - ), - Kustomization( - name="cluster", - namespace="flux-system", - path="./kubernetes/flux", - ), - ], - # Third traversal - [ - Kustomization( - name="cluster-apps-rook-ceph", - namespace="flux-system", - path="./kubernetes/apps/rook-ceph/rook-ceph/app", - ), - Kustomization( - name="cluster-apps-volsync", - namespace="flux-system", - path="./kubernetes/apps/volsync/volsync/app", - ), - ], - [], - [], - # The returned kustomizations point to subdirectories that have - # already been searched so no need to search further. - ] - paths = [] - - async def fetch( - root: Path, p: Path, build: bool, sources: list[Source] - ) -> list[Kustomization]: - nonlocal paths, results - paths.append((str(root), str(p))) - return results.pop(0) - - with patch("flux_local.git_repo.PathSelector.root", Path("/home/example")), patch( - "flux_local.git_repo.get_fluxtomizations", fetch - ): - kustomizations = await kustomization_traversal( - root_path_selector=PathSelector(path=Path(path)), - path_selector=PathSelector(path=Path(path)), - build=True, - ) - assert len(kustomizations) == 5 - assert paths == [ - ("/home/example", "kubernetes/flux"), - ("/home/example", "kubernetes/cluster"), - ("/home/example", "kubernetes/apps"), - ("/home/example", "kubernetes/apps/rook-ceph/rook-ceph/app"), - ("/home/example", "kubernetes/apps/volsync/volsync/app"), - ] + assert visits == snapshot def test_source() -> None: @@ -366,7 +180,7 @@ def test_is_allowed_source() -> None: path="./kubernetes/apps/volsync/volsync/app", source_name="flux-system", ) - assert is_allowed_source(ks, [Source.from_str("flux-system")]) + assert is_allowed_source([Source.from_str("flux-system")])(ks) def test_is_not_allowed_source() -> None: @@ -377,7 +191,7 @@ def test_is_not_allowed_source() -> None: path="./kubernetes/apps/volsync/volsync/app", source_name="flux-system", ) - assert not is_allowed_source(ks, [Source.from_str("flux-system-other")]) + assert not is_allowed_source([Source.from_str("flux-system-other")])(ks) def test_is_allowed_source_namespace_optional() -> None: @@ -389,6 +203,82 @@ def test_is_allowed_source_namespace_optional() -> None: source_name="flux-system", source_namespace="flux-system2", ) - assert is_allowed_source(ks, [Source.from_str("flux-system")]) - assert is_allowed_source(ks, [Source.from_str("flux-system2/flux-system")]) - assert not is_allowed_source(ks, [Source.from_str("flux-system3/flux-system")]) + assert is_allowed_source([Source.from_str("flux-system")])(ks) + assert is_allowed_source([Source.from_str("flux-system2/flux-system")])(ks) + assert not is_allowed_source([Source.from_str("flux-system3/flux-system")])(ks) + + +@pytest.mark.parametrize( + ("path", "sources"), + [ + ("tests/testdata/cluster", None), + ("tests/testdata/cluster2", None), + ( + "tests/testdata/cluster3", + [Source.from_str("cluster=tests/testdata/cluster3")], + ), + ("tests/testdata/cluster4", None), + ("tests/testdata/cluster5", None), + ("tests/testdata/cluster6", None), + ("tests/testdata/cluster7", None), + ], + ids=[ + "cluster", + "cluster2", + "cluster3", + "cluster4", + "cluster5", + "cluster6", + "cluster7", + ], +) +async def test_internal_commands( + path: str, sources: list[Source] | None, snapshot: SnapshotAssertion +) -> None: + """Tests that trace internal command run for each step.""" + + context_cmds: dict[str, Any] = {} + + async def piped_recorder(tasks: Sequence[Task]) -> str: + """Record commands run with the trace context.""" + + piped_cmds = [] + for task in tasks: + if isinstance(task, Stash): + continue + piped_cmds.append(str(task)) + + c = context_cmds + for level in trace.get(["root"]): + if level not in c: + c[level] = {} + c = c[level] + c["cmds"] = c.get("cmds", []) + piped_cmds + return await run_piped(tasks) + + selector = ResourceSelector(path=PathSelector(path=Path(path), sources=sources)) + with patch("flux_local.kustomize.run_piped", side_effect=piped_recorder): + await build_manifest(selector=selector) + + assert context_cmds == snapshot + + +@pytest.mark.parametrize( + ("path", "expected_path"), + [ + ("kubernetes/apps/example", "kubernetes/apps/example"), + ("./kubernetes/apps/example", "kubernetes/apps/example"), + ("/kubernetes/apps/example", "kubernetes/apps/example"), + ("", str(TESTDATA)), + ], +) +def test_adjust_ks_path(path: str, expected_path: str) -> None: + """Test adjusting the Kustomization path relative directory.""" + ks = Kustomization( + name="ks", + namespace="flux-system", + path=path, + source_name="flux-system", + ) + selector = PathSelector(TESTDATA_FULL_PATH) + assert adjust_ks_path(ks, selector) == Path(expected_path) diff --git a/tests/test_helm.py b/tests/test_helm.py index 4bf6909e..6f77b9f9 100644 --- a/tests/test_helm.py +++ b/tests/test_helm.py @@ -1,14 +1,26 @@ """Tests for helm library.""" +import base64 from pathlib import Path from typing import Any, Generator import pytest from aiofiles.os import mkdir +from syrupy.assertion import SnapshotAssertion from flux_local import kustomize -from flux_local.helm import Helm -from flux_local.manifest import HelmRelease, HelmRepository +from flux_local.exceptions import HelmException +from flux_local.helm import Helm, expand_value_references +from flux_local.manifest import ( + HelmRelease, + HelmRepository, + ValuesReference, + Secret, + ConfigMap, + Kustomization, + HelmChart, +) +from flux_local.git_repo import ResourceSelector, PathSelector, build_manifest REPO_DIR = Path("tests/testdata/cluster/infrastructure/configs") RELEASE_DIR = Path("tests/testdata/cluster/infrastructure/controllers") @@ -62,3 +74,353 @@ async def test_template(helm: Helm, helm_releases: list[dict[str, Any]]) -> None docs = await obj.grep("kind=ServiceAccount").objects() names = [doc.get("metadata", {}).get("name") for doc in docs] assert names == ["metallb-controller", "metallb-speaker"] + + +async def test_value_references(snapshot: SnapshotAssertion) -> None: + """Test for expanding value references.""" + path = Path("tests/testdata/cluster8") + selector = ResourceSelector(path=PathSelector(path=path)) + manifest = await build_manifest(selector=selector) + assert len(manifest.clusters) == 1 + assert len(manifest.clusters[0].kustomizations) == 2 + ks = manifest.clusters[0].kustomizations[0] + assert ks.name == "apps" + assert len(ks.helm_releases) == 1 + hr = ks.helm_releases[0] + assert hr.name == "podinfo" + assert hr.values == snapshot + + +def test_values_references_with_values_key() -> None: + """Test for expanding a value reference with a values key.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={"test": "test"}, + values_from=[ + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-values-configmap", + valuesKey="some-key", + targetPath="target.path", + ), + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-binary-data-configmap", + valuesKey="some-key", + ), + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + config_maps=[ + ConfigMap( + name="test-values-configmap", + namespace="test", + data={"some-key": "example_value"}, + ), + ConfigMap( + name="test-binary-data-configmap", + namespace="test", + binary_data={ + "some-key": base64.b64encode( + "encoded_key: encoded_value".encode("utf-8") + ) + }, + ), + ], + ) + updated_hr = expand_value_references(hr, ks) + assert updated_hr.values == { + "test": "test", + "target": { + "path": "example_value", + }, + "encoded_key": "encoded_value", + } + + +def test_values_references_with_missing_values_key() -> None: + """Test for expanding a value reference with a values key that is missing.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={"test": "test"}, + values_from=[ + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-values-configmap", + valuesKey="some-key-does-not-exist", + targetPath="target.path", + ) + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + config_maps=[ + ConfigMap( + name="test-values-configmap", + namespace="test", + data={"some-key": "example_value"}, + ) + ], + ) + updated_hr = expand_value_references(hr, ks) + assert updated_hr.values == { + "test": "test", + } + + +def test_values_references_invalid_yaml() -> None: + """Test for expanding a value reference with a values key.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={"test": "test"}, + values_from=[ + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-values-configmap", + ) + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + config_maps=[ + ConfigMap( + name="test-values-configmap", + namespace="test", + data={"values.yaml": "not-yaml"}, + ) + ], + ) + with pytest.raises(HelmException, match=r"valid yaml"): + expand_value_references(hr, ks) + + +def test_values_references_invalid_binary_data() -> None: + """Test for expanding a value reference with an invalid binary data key.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={"test": "test"}, + values_from=[ + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-values-configmap", + ) + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + config_maps=[ + ConfigMap( + name="test-values-configmap", + namespace="test", + binary_data={"values.yaml": "this is not base64 data"}, + ) + ], + ) + with pytest.raises(HelmException, match=r"Unable to decode binary data"): + expand_value_references(hr, ks) + + +def test_values_reference_invalid_target_path() -> None: + """Test for expanding a value reference with a values key.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={ + "test": "test", + "target": ["a", "b", "c"], + }, + values_from=[ + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-values-configmap", + valuesKey="some-key", + # Target above is a list + targetPath="target.path", + ) + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + config_maps=[ + ConfigMap( + name="test-values-configmap", + namespace="test", + data={"some-key": "example_value"}, + ) + ], + ) + with pytest.raises(HelmException, match=r"values to be a dict"): + expand_value_references(hr, ks) + + +def test_values_reference_invalid_configmap_and_secret() -> None: + """Test a values reference to a config map and secret that do not exist.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={"test": "test"}, + values_from=[ + ValuesReference( + kind="ConfigMap", + namespace="test", + name="test-values-configmap", + optional=False, # We just log + ), + ValuesReference( + kind="Secret", + namespace="test", + name="test-values-secret", + optional=False, # We just log + ), + ValuesReference( + kind="UnknownKind", + namespace="test", + name="test-values-secret", + ), + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + config_maps=[], + ) + updated_hr = expand_value_references(hr, ks) + # No changes to the values + assert updated_hr.values == {"test": "test"} + + +def test_values_references_secret() -> None: + """Test for expanding a value reference for a secret.""" + hr = HelmRelease( + name="test", + namespace="test", + chart=HelmChart( + repo_name="test-repo", + repo_namespace="flux-system", + name="test-chart", + version="test-version", + ), + values={"test": "test"}, + values_from=[ + ValuesReference( + kind="Secret", + namespace="test", + name="test-values-secret", + valuesKey="some-key1", + targetPath="target.path1", + ), + ValuesReference( + kind="Secret", + namespace="test", + name="test-string-values-secret", + valuesKey="some-key2", + targetPath="target.path2", + ), + ], + ) + ks = Kustomization( + name="test", + namespace="test", + path="example/path", + helm_releases=[hr], + secrets=[ + Secret.parse_doc( + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "test-values-secret", + "namespace": "test", + }, + "data": { + "some-key1": base64.b64encode("example-value".encode("utf-8")), + }, + } + ), + Secret.parse_doc( + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "test-string-values-secret", + "namespace": "test", + }, + "stringData": { + "some-key2": "example-string-value", + }, + } + ), + ], + ) + updated_hr = expand_value_references(hr, ks) + assert updated_hr.values == { + "test": "test", + "target": { + "path1": "**PLACEHOLDER**", + "path2": "**PLACEHOLDER**", + }, + } diff --git a/tests/test_image.py b/tests/test_image.py new file mode 100644 index 00000000..7aa850b3 --- /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", "busybox"}}, + ), + ( + 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/test_kustomize.py b/tests/test_kustomize.py index bfc664da..1827fdf1 100644 --- a/tests/test_kustomize.py +++ b/tests/test_kustomize.py @@ -3,8 +3,10 @@ from pathlib import Path import pytest +from syrupy.assertion import SnapshotAssertion +import yaml -from flux_local import kustomize, exceptions +from flux_local import kustomize, exceptions, manifest TESTDATA_DIR = Path("tests/testdata") @@ -20,49 +22,26 @@ "path", [TESTDATA_DIR / "repo", (TESTDATA_DIR / "repo").absolute()], ) -async def test_build(path: Path) -> None: - """Test a kustomize build command.""" - result = await kustomize.build(path).run() - assert "Secret" in result - assert "ConfigMap" in result - assert result == (TESTDATA_DIR / "repo/all.golden").read_text() - - -@pytest.mark.parametrize( - "path", - [TESTDATA_DIR / "repo", (TESTDATA_DIR / "repo").absolute()], -) -async def test_build_grep(path: Path) -> None: - """Test a kustomize build and grep command chained.""" - result = await kustomize.build(path).grep("kind=ConfigMap").run() - assert "Secret" not in result - assert "ConfigMap" in result - assert result == (TESTDATA_DIR / "repo/configmap.build.golden").read_text() - - -@pytest.mark.parametrize( - "path", - [TESTDATA_DIR / "repo", (TESTDATA_DIR / "repo").absolute()], -) -async def test_grep(path: Path) -> None: +async def test_grep(path: Path, snapshot: SnapshotAssertion) -> None: """Test a kustomize grep command.""" result = await kustomize.grep("kind=ConfigMap", path).run() assert "Secret" not in result assert "ConfigMap" in result - assert result == (TESTDATA_DIR / "repo/configmap.grep.golden").read_text() + assert result == snapshot @pytest.mark.parametrize( "path", [TESTDATA_DIR / "repo", (TESTDATA_DIR / "repo").absolute()], ) -async def test_objects(path: Path) -> None: +async def test_objects(path: Path, snapshot: SnapshotAssertion) -> None: """Test loading yaml documents.""" - cmd = kustomize.build(path).grep("kind=ConfigMap") + cmd = kustomize.grep("kind=ConfigMap", path) result = await cmd.objects() assert len(result) == 1 assert result[0].get("kind") == "ConfigMap" assert result[0].get("apiVersion") == "v1" + assert result == snapshot @pytest.mark.parametrize( @@ -71,7 +50,7 @@ async def test_objects(path: Path) -> None: ) async def test_stash(path: Path) -> None: """Test loading yaml documents.""" - cmd = await kustomize.build(path).stash() + cmd = await kustomize.grep("kind=Ignored", path, invert=True).stash() result = await cmd.grep("kind=ConfigMap").objects() assert len(result) == 1 assert result[0].get("kind") == "ConfigMap" @@ -90,7 +69,7 @@ async def test_stash(path: Path) -> None: ) async def test_validate_pass(path: Path) -> None: """Test applying policies to validate resources.""" - cmd = kustomize.build(path) + cmd = kustomize.grep("kind=ConfigMap", path) await cmd.validate(TESTDATA_DIR / "policies/pass.yaml") @@ -100,90 +79,69 @@ async def test_validate_pass(path: Path) -> None: ) async def test_validate_fail(path: Path) -> None: """Test applying policies to validate resources.""" - cmd = kustomize.build(path) + cmd = kustomize.grep("kind=ConfigMap", path) with pytest.raises( - exceptions.CommandException, match="require-test-annotation: validation error" - ): + exceptions.CommandException, match="fail: 1"): await cmd.validate(TESTDATA_DIR / "policies/fail.yaml") -async def test_cannot_kustomize(tmp_path: Path) -> None: - """Test that empty directories cannot be kustomized.""" - assert not await kustomize.can_kustomize_dir(tmp_path) - - -async def test_can_kustomize(tmp_path: Path) -> None: - """Test that empty directories cannot be kustomized.""" - ks = tmp_path / "kustomization.yaml" - ks.write_text(KUSTOMIZATION) - assert await kustomize.can_kustomize_dir(tmp_path) - - -async def test_fluxtomize_file(tmp_path: Path) -> None: - """Test implicit kustomization of files in a directory.""" - settings = (TESTDATA_DIR / "repo/cluster-settings.yaml").read_text() - example_yaml = tmp_path / "example.yaml" - example_yaml.write_text(settings) - - content = await kustomize.fluxtomize(tmp_path) - assert content - assert content.decode("utf-8").split("\n") == [ - "---", - "apiVersion: v1", - "kind: ConfigMap", - "metadata:", - " namespace: flux-system", - " name: cluster-settings", - "data:", - " CLUSTER: dev", - " DOMAIN: example.org", - "", - ] - - -async def test_fluxtomize_subdir(tmp_path: Path) -> None: - """Test implicit kustomization of subdirectories that can be kustomized.""" - subdir = tmp_path / "subdir" - subdir.mkdir() - ks = subdir / "kustomization.yaml" - ks.write_text(KUSTOMIZATION) - - settings = (TESTDATA_DIR / "repo/cluster-settings.yaml").read_text() - example_yaml = subdir / "example.yaml" - example_yaml.write_text(settings) - - content = await kustomize.fluxtomize(tmp_path) - assert content - assert content.decode("utf-8").split("\n") == [ - "---", - "apiVersion: v1", - "data:", - " CLUSTER: dev", - " DOMAIN: example.org", - "kind: ConfigMap", - "metadata:", - " name: cluster-settings", - " namespace: flux-system", - "", - ] - - -async def test_fluxtomize_ignores_empty_subdir(tmp_path: Path) -> None: - """Test implicit kustomization.""" - subdir = tmp_path / "subdir" - subdir.mkdir() - - content = await kustomize.fluxtomize(tmp_path) - assert not content - - -async def test_build_flags() -> None: - """Test a kustomize build command with extra flags.""" - result = await kustomize.build( - TESTDATA_DIR / "repo", - # Duplicates existing flags, should be a no-op - kustomize_flags=["--load-restrictor=LoadRestrictionsNone"], - ).run() - assert "Secret" in result - assert "ConfigMap" in result - assert result == (TESTDATA_DIR / "repo/all.golden").read_text() +async def test_target_namespace() -> None: + """Test a kustomization with a target namespace.""" + ks = kustomize.grep("kind=ConfigMap", TESTDATA_DIR / "repo") + + result = await ks.objects() + assert len(result) == 1 + config_map = result[0] + assert "metadata" in config_map + assert config_map["metadata"] == { + "name": "cluster-settings", + "namespace": "flux-system", + "annotations": { + "config.kubernetes.io/index": "0", + "config.kubernetes.io/path": "cluster-settings.yaml", + "internal.config.kubernetes.io/index": "0", + "internal.config.kubernetes.io/path": "cluster-settings.yaml", + }, + } + + result = await ks.objects(target_namespace="configs") + assert len(result) == 1 + config_map = result[0] + assert "metadata" in config_map + assert config_map["metadata"] == { + "name": "cluster-settings", + # Verify updated namespace + "namespace": "configs", + "annotations": { + "config.kubernetes.io/index": "0", + "config.kubernetes.io/path": "cluster-settings.yaml", + "internal.config.kubernetes.io/index": "0", + "internal.config.kubernetes.io/path": "cluster-settings.yaml", + }, + } + + +async def test_flux_build_path_is_not_dir() -> None: + """Test case where the flux build path does not exist.""" + cmd = kustomize.flux_build( + manifest.Kustomization(name="example", path="./"), + Path(TESTDATA_DIR) / "does-not-exist", + ) + with pytest.raises(exceptions.FluxException, match="not a directory"): + await cmd.objects() + + +async def test_flux_build() -> None: + """Test flux build cli.""" + docs = list( + yaml.safe_load_all( + Path( + f"{TESTDATA_DIR}/cluster/clusters/prod/flux-system/gotk-sync.yaml" + ).read_text() + ) + ) + assert len(docs) == 2 + ks = manifest.Kustomization.parse_doc(docs[1]) + cmd = kustomize.flux_build(ks, Path(ks.path)) + result = await cmd.run() + assert "GitRepository" in result diff --git a/tests/test_manifest.py b/tests/test_manifest.py index d47fdcd3..22604343 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -102,8 +102,6 @@ async def test_serializing_manifest(tmp_path: Path) -> None: manifest = Manifest( clusters=[ Cluster( - name="cluster", - namespace="flux-system", path="./example", kustomizations=[], ) @@ -111,15 +109,13 @@ async def test_serializing_manifest(tmp_path: Path) -> None: ) await write_manifest(tmp_path / "file.yaml", manifest) new_manifest = await read_manifest(tmp_path / "file.yaml") - assert new_manifest.dict() == { + assert new_manifest.model_dump() == { "clusters": [ { - "name": "cluster", - "namespace": "flux-system", "path": "./example", "kustomizations": [], }, ] } - assert new_manifest.compact_dict() == new_manifest.dict() - assert new_manifest.compact_dict() == manifest.dict() + assert new_manifest.compact_dict() == new_manifest.model_dump() + assert new_manifest.compact_dict() == manifest.model_dump() diff --git a/tests/testdata/cluster2/apps/networking/ingress-nginx/app/helmrelease.yaml b/tests/testdata/cluster2/apps/networking/ingress-nginx/app/helmrelease.yaml index ed75b7e0..bfd2943c 100644 --- a/tests/testdata/cluster2/apps/networking/ingress-nginx/app/helmrelease.yaml +++ b/tests/testdata/cluster2/apps/networking/ingress-nginx/app/helmrelease.yaml @@ -3,7 +3,6 @@ apiVersion: helm.toolkit.fluxcd.io/v2beta1 kind: HelmRelease metadata: name: ingress-nginx - namespace: networking spec: interval: 15m chart: diff --git a/tests/testdata/cluster2/apps/networking/ingress-nginx/app/kustomization.yaml b/tests/testdata/cluster2/apps/networking/ingress-nginx/app/kustomization.yaml index 7e660d66..e8640cd7 100644 --- a/tests/testdata/cluster2/apps/networking/ingress-nginx/app/kustomization.yaml +++ b/tests/testdata/cluster2/apps/networking/ingress-nginx/app/kustomization.yaml @@ -1,7 +1,6 @@ --- apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: networking resources: - ./helmrelease.yaml labels: diff --git a/tests/testdata/cluster2/apps/networking/ingress-nginx/ks.yaml b/tests/testdata/cluster2/apps/networking/ingress-nginx/ks.yaml index db101895..9e24a4bd 100644 --- a/tests/testdata/cluster2/apps/networking/ingress-nginx/ks.yaml +++ b/tests/testdata/cluster2/apps/networking/ingress-nginx/ks.yaml @@ -23,6 +23,7 @@ metadata: name: cluster-apps-ingress-nginx namespace: flux-system spec: + targetNamespace: networking dependsOn: - name: cluster-apps-ingress-nginx-certificates path: ./tests/testdata/cluster2/apps/networking/ingress-nginx/app diff --git a/tests/testdata/cluster3/namespaces/base/podinfo/kustomization.yaml b/tests/testdata/cluster3/namespaces/base/podinfo/kustomization.yaml index 809cbe53..6d4c15d6 100644 --- a/tests/testdata/cluster3/namespaces/base/podinfo/kustomization.yaml +++ b/tests/testdata/cluster3/namespaces/base/podinfo/kustomization.yaml @@ -2,4 +2,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: + - repository.yaml - namespace.yaml + - release.yaml diff --git a/tests/testdata/cluster3/namespaces/base/podinfo/release.yaml b/tests/testdata/cluster3/namespaces/base/podinfo/release.yaml new file mode 100644 index 00000000..df449d33 --- /dev/null +++ b/tests/testdata/cluster3/namespaces/base/podinfo/release.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: podinfo + namespace: podinfo +spec: + releaseName: podinfo + chart: + spec: + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + interval: 50m + install: + remediation: + retries: 3 + # Default values + # https://github.com/stefanprodan/podinfo/blob/master/charts/podinfo/values.yaml + values: + redis: + enabled: true + repository: public.ecr.aws/docker/library/redis + tag: 7.0.6 + ingress: + enabled: true + className: nginx diff --git a/tests/testdata/cluster3/namespaces/base/podinfo/repository.yaml b/tests/testdata/cluster3/namespaces/base/podinfo/repository.yaml new file mode 100644 index 00000000..fed1bb49 --- /dev/null +++ b/tests/testdata/cluster3/namespaces/base/podinfo/repository.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 5m + type: oci + url: oci://ghcr.io/stefanprodan/charts diff --git a/tests/testdata/cluster6/apps/renovate/release.yaml b/tests/testdata/cluster6/apps/renovate/release.yaml index 27c25952..43266d72 100644 --- a/tests/testdata/cluster6/apps/renovate/release.yaml +++ b/tests/testdata/cluster6/apps/renovate/release.yaml @@ -8,6 +8,7 @@ spec: chart: spec: chart: renovate + version: 37.64.3 sourceRef: kind: HelmRepository name: renovate diff --git a/tests/testdata/cluster7/clusters/home/charts.yaml b/tests/testdata/cluster7/clusters/home/charts.yaml index 86332919..d6869ccb 100644 --- a/tests/testdata/cluster7/clusters/home/charts.yaml +++ b/tests/testdata/cluster7/clusters/home/charts.yaml @@ -6,7 +6,7 @@ metadata: namespace: flux-system spec: interval: 10m0s - path: ./tests/testdata/cluster7/flux/charts + path: /tests/testdata/cluster7/flux/charts prune: true sourceRef: kind: GitRepository 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..eb70af57 --- /dev/null +++ b/tests/testdata/cluster8/apps/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - pods.yaml + - podinfo.yaml + - podinfo-values.yaml + - podinfo-tls-values.yaml diff --git a/tests/testdata/cluster8/apps/podinfo-tls-values.yaml b/tests/testdata/cluster8/apps/podinfo-tls-values.yaml new file mode 100644 index 00000000..3bed1343 --- /dev/null +++ b/tests/testdata/cluster8/apps/podinfo-tls-values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: podinfo-tls-values +data: + crt: dmFsdWUtMg0KDQo= diff --git a/tests/testdata/cluster8/apps/podinfo-values.yaml b/tests/testdata/cluster8/apps/podinfo-values.yaml new file mode 100644 index 00000000..1c6a9b8b --- /dev/null +++ b/tests/testdata/cluster8/apps/podinfo-values.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: podinfo + name: podinfo-values +data: + values.yaml: |- + redis: + enabled: true + repository: public.ecr.aws/docker/library/redis + tag: 7.0.6 + ingress: + enabled: true + className: nginx + hosts: + - host: podinfo.production + paths: + - path: / + pathType: ImplementationSpecific diff --git a/tests/testdata/cluster8/apps/podinfo.yaml b/tests/testdata/cluster8/apps/podinfo.yaml new file mode 100644 index 00000000..6835e102 --- /dev/null +++ b/tests/testdata/cluster8/apps/podinfo.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 5m + type: oci + url: oci://ghcr.io/stefanprodan/charts +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: podinfo + namespace: podinfo +spec: + releaseName: podinfo + chart: + spec: + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + interval: 50m + install: + remediation: + retries: 3 + valuesFrom: + - kind: ConfigMap + name: podinfo-values + - kind: Secret + name: podinfo-tls-values + valuesKey: crt + targetPath: tls.crt + optional: true diff --git a/tests/testdata/cluster8/apps/pods.yaml b/tests/testdata/cluster8/apps/pods.yaml new file mode 100644 index 00000000..05d8839a --- /dev/null +++ b/tests/testdata/cluster8/apps/pods.yaml @@ -0,0 +1,31 @@ +--- +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"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: sleep + namespace: home +spec: + initContainers: + - name: sleep + image: busybox + 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/testdata/repo/all.golden b/tests/testdata/repo/all.golden deleted file mode 100644 index b71883bd..00000000 --- a/tests/testdata/repo/all.golden +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -data: - CLUSTER: dev - DOMAIN: example.org -kind: ConfigMap -metadata: - name: cluster-settings - namespace: flux-system ---- -apiVersion: v1 -data: - PWD: secret -kind: Secret -metadata: - name: cluster-secrets - namespace: flux-system diff --git a/tests/testdata/repo/configmap.build.golden b/tests/testdata/repo/configmap.build.golden deleted file mode 100644 index 99376b5f..00000000 --- a/tests/testdata/repo/configmap.build.golden +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -data: - CLUSTER: dev - DOMAIN: example.org -kind: ConfigMap -metadata: - name: cluster-settings - namespace: flux-system - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' diff --git a/tests/testdata/repo/configmap.grep.golden b/tests/testdata/repo/configmap.grep.golden deleted file mode 100644 index 0bee8ad7..00000000 --- a/tests/testdata/repo/configmap.grep.golden +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - namespace: flux-system - name: cluster-settings - annotations: - config.kubernetes.io/index: '0' - config.kubernetes.io/path: 'cluster-settings.yaml' - internal.config.kubernetes.io/index: '0' - internal.config.kubernetes.io/path: 'cluster-settings.yaml' -data: - CLUSTER: dev - DOMAIN: example.org diff --git a/tests/tool/__init__.py b/tests/tool/__init__.py new file mode 100644 index 00000000..92a42df9 --- /dev/null +++ b/tests/tool/__init__.py @@ -0,0 +1,9 @@ +"""Test helpers for flux-local tools.""" + +from flux_local.command import Command, run + +FLUX_LOCAL_BIN = "flux-local" + + +async def run_command(args: list[str], env: dict[str, str] | None = None) -> str: + return await run(Command([FLUX_LOCAL_BIN] + args, env=env)) diff --git a/tests/tool/__snapshots__/test_build.ambr b/tests/tool/__snapshots__/test_build.ambr new file mode 100644 index 00000000..387b5f7f --- /dev/null +++ b/tests/tool/__snapshots__/test_build.ambr @@ -0,0 +1,3301 @@ +# serializer version: 1 +# name: test_build[build-cluster2] + ''' + --- + apiVersion: v1 + kind: Namespace + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster-apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + kustomize.toolkit.fluxcd.io/prune: disabled + name: monitoring + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster-apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: cluster-apps-kubernetes-dashboard + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + healthChecks: + - apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + name: kubernetes-dashboard + namespace: monitoring + interval: 30m + path: ./tests/testdata/cluster2/apps/monitoring/kubernetes-dashboard/app + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: home-ops-kubernetes + timeout: 3m + --- + apiVersion: v1 + kind: Namespace + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster-apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + kustomize.toolkit.fluxcd.io/prune: disabled + name: networking + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster-apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: cluster-apps-ingress-nginx-certificates + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + dependsOn: + - name: cluster-apps-cert-manager-issuers + interval: 30m + path: ./tests/testdata/cluster2/apps/networking/ingress-nginx/certificates + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: home-ops-kubernetes + timeout: 3m + wait: true + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster-apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: cluster-apps-ingress-nginx + namespace: flux-system + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + dependsOn: + - name: cluster-apps-ingress-nginx-certificates + healthChecks: + - apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + name: ingress-nginx + namespace: networking + interval: 30m + path: ./tests/testdata/cluster2/apps/networking/ingress-nginx/app + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: home-ops-kubernetes + targetNamespace: networking + timeout: 3m + + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + app.kubernetes.io/instance: kubernetes-dashboard + app.kubernetes.io/name: kubernetes-dashboard + kustomize.toolkit.fluxcd.io/name: cluster-apps-kubernetes-dashboard + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: kubernetes-dashboard + namespace: monitoring + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + chart: + spec: + chart: kubernetes-dashboard + sourceRef: + kind: HelmRepository + name: kubernetes-dashboard + namespace: flux-system + version: 6.0.0 + install: + createNamespace: true + remediation: + retries: 3 + interval: 15m + maxHistory: 3 + uninstall: + keepHistory: false + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + env: + TZ: America/New_York + extraArgs: + - --enable-skip-login + - --disable-settings-authorizer + - --enable-insecure-login + - --token-ttl=43200 + ingress: + annotations: + hajimari.io/icon: mdi:kubernetes + nginx.ingress.kubernetes.io/whitelist-source-range: | + 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 + className: nginx + enabled: true + hosts: + - kubernetes.devbu.io + tls: + - hosts: + - kubernetes.devbu.io + metricsScraper: + enabled: true + serviceMonitor: + enabled: false + + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + kustomize.toolkit.fluxcd.io/name: cluster-apps-ingress-nginx + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: ingress-nginx + namespace: networking + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + chart: + spec: + chart: ingress-nginx + sourceRef: + kind: HelmRepository + name: ingress-nginx + namespace: flux-system + version: 4.5.2 + install: + createNamespace: true + remediation: + retries: 3 + interval: 15m + maxHistory: 3 + uninstall: + keepHistory: false + upgrade: + cleanupOnFail: true + remediation: + retries: 3 + values: + controller: + config: + client-body-buffer-size: 100M + client-body-timeout: 120 + client-header-timeout: 120 + enable-brotli: "true" + enable-real-ip: "true" + forwarded-for-header: CF-Connecting-IP + hsts-max-age: "31449600" + keep-alive: 120 + keep-alive-requests: 10000 + log-format-escape-json: "true" + log-format-upstream: | + {"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for", "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", "request_length": $request_length, "duration": $request_time,"method": "$request_method", "http_referrer": "$http_referer", "http_user_agent": "$http_user_agent"} + proxy-body-size: 0 + proxy-buffer-size: 16k + ssl-protocols: TLSv1.3 TLSv1.2 + use-forwarded-headers: "true" + extraArgs: + default-ssl-certificate: networking/devbu-io-tls + extraEnvs: + - name: TZ + value: America/New_York + ingressClassResource: + default: true + metrics: + enabled: true + serviceMonitor: + enabled: true + namespace: networking + namespaceSelector: + any: true + podAnnotations: + configmap.reloader.stakater.com/reload: cloudflare-proxied-networks + replicaCount: 2 + resources: + limits: + memory: 500Mi + requests: + cpu: 10m + memory: 250Mi + service: + externalIPs: + - ${SVC_NGINX_ADDR} + externalTrafficPolicy: Local + topologySpreadConstraints: + - labelSelector: + matchLabels: + app.kubernetes.io/component: controller + app.kubernetes.io/name: ingress-nginx + maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + defaultBackend: + enabled: false + + --- + apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster-apps-ingress-nginx-certificates + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: example-io + namespace: networking + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + commonName: example.io + dnsNames: + - example.io + issuerRef: + kind: ClusterIssuer + name: letsencrypt-production + secretName: example-io-tls + + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: cluster-apps + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + interval: 10m + path: ./tests/testdata/cluster2/apps + prune: true + sourceRef: + kind: GitRepository + name: home-ops-kubernetes + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: OCIRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-manifests + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + interval: 10m + ref: + tag: v0.40.0 + url: oci://ghcr.io/fluxcd/flux-manifests + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m + path: ./ + prune: true + sourceRef: + kind: OCIRepository + name: flux-manifests + wait: true + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: home-ops-kubernetes + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + ignore: | + # exclude all + /* + # include kubernetes directory + !/tests/testdata/cluster2/kubernetes + interval: 30m + ref: + branch: main + secretRef: + name: github-deploy-key + url: ssh://git@github.com/example/home-ops + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: cluster + namespace: flux-system + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + interval: 30m + path: ./tests/testdata/cluster2/flux + prune: true + sourceRef: + kind: GitRepository + name: home-ops-kubernetes + wait: false + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: ingress-nginx + namespace: flux-system + annotations: + config.kubernetes.io/index: '5' + internal.config.kubernetes.io/index: '5' + spec: + interval: 2h + url: https://kubernetes.github.io/ingress-nginx + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: kubernetes-dashboard + namespace: flux-system + annotations: + config.kubernetes.io/index: '6' + internal.config.kubernetes.io/index: '6' + spec: + interval: 2h + url: https://kubernetes.github.io/dashboard/ + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: cluster + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: kyverno + namespace: flux-system + annotations: + config.kubernetes.io/index: '7' + internal.config.kubernetes.io/index: '7' + spec: + interval: 2h + url: https://kyverno.github.io/kyverno/ + + + ''' +# --- +# name: test_build[build-cluster3] + ''' + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: OCIRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: cluster + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + interval: 1m + ref: + tag: main + url: oci://ghcr.io/allenporter/manifests/flux-local + verify: + provider: cosign + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + 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: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m0s + path: ./tests/testdata/cluster3/clusters/cluster3 + prune: true + sourceRef: + kind: GitRepository + name: flux-system + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: namespaces + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + interval: 10m + path: ./namespaces/overlays/cluster3 + prune: true + sourceRef: + kind: OCIRepository + name: cluster + timeout: 30m + wait: false + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: tenants + namespace: flux-system + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + interval: 10m0s + path: ./tenants/overlays/cluster3 + prune: true + sourceRef: + kind: OCIRepository + name: cluster + wait: false + + + ''' +# --- +# name: test_build[build-cluster6] + ''' + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: renovate + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + interval: 30m + url: https://docs.renovatebot.com/helm-charts + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: renovate + namespace: default + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + chart: + spec: + chart: renovate + sourceRef: + kind: HelmRepository + name: renovate + namespace: flux-system + version: 37.64.3 + interval: 5m + values: + renovate: + existingConfigFile: /dev/null + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + limits: + cpu: 1000m + memory: 2G + requests: + cpu: 100m + memory: 256Mi + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: Always + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + serviceAccount: + create: true + + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: apps + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + path: ./tests/testdata/cluster6/apps/ + sourceRef: + kind: GitRepository + name: flux-system + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + 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: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m0s + path: ./tests/testdata/cluster6/cluster + prune: true + sourceRef: + kind: GitRepository + name: flux-system + + + ''' +# --- +# name: test_build[build-helm-cluster6] + ''' + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: renovate + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + interval: 30m + url: https://docs.renovatebot.com/helm-charts + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: renovate + namespace: default + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + chart: + spec: + chart: renovate + sourceRef: + kind: HelmRepository + name: renovate + namespace: flux-system + version: 37.64.3 + interval: 5m + values: + renovate: + existingConfigFile: /dev/null + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + limits: + cpu: 1000m + memory: 2G + requests: + cpu: 100m + memory: 256Mi + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: Always + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + serviceAccount: + create: true + + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: apps + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + path: ./tests/testdata/cluster6/apps/ + sourceRef: + kind: GitRepository + name: flux-system + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + 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: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m0s + path: ./tests/testdata/cluster6/cluster + prune: true + sourceRef: + kind: GitRepository + name: flux-system + + --- + # Source: renovate/templates/serviceaccount.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: renovate + labels: + helm.sh/chart: renovate-37.64.3 + app.kubernetes.io/name: renovate + app.kubernetes.io/instance: renovate + app.kubernetes.io/version: "37.64.3" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + --- + # Source: renovate/templates/cronjob.yaml + apiVersion: batch/v1 + kind: CronJob + metadata: + name: renovate + labels: + helm.sh/chart: renovate-37.64.3 + app.kubernetes.io/name: renovate + app.kubernetes.io/instance: renovate + app.kubernetes.io/version: "37.64.3" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + schedule: "0 1 * * *" + jobTemplate: + metadata: + labels: + app.kubernetes.io/name: renovate + app.kubernetes.io/instance: renovate + spec: + template: + metadata: + labels: + app.kubernetes.io/name: renovate + app.kubernetes.io/instance: renovate + spec: + serviceAccountName: renovate + restartPolicy: Never + containers: + - name: renovate + image: "ghcr.io/renovatebot/renovate:37.64.3" + imagePullPolicy: IfNotPresent + env: + - name: RENOVATE_CONFIG_FILE + value: /dev/null + envFrom: + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + resources: + limits: + cpu: 1000m + memory: 2G + requests: + cpu: 100m + memory: 256Mi + volumes: + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: Always + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + + + ''' +# --- +# name: test_build[build-helm-cluster8-valuesFrom] + ''' + --- + apiVersion: v1 + kind: Pod + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: mnt-pod + namespace: home + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + containers: + - args: + - -c + - sleep 500000 + command: + - /bin/sh + image: alpine + name: task-pv-container + volumeMounts: + - mountPath: /data + name: task-pv-storage + volumes: + - name: task-pv-storage + persistentVolumeClaim: + claimName: some-pod-datadir + --- + apiVersion: v1 + kind: Pod + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: sleep + namespace: home + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + initContainers: + - args: + - -c + - sleep 500000 + command: + - /bin/sh + image: busybox + name: sleep + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 5m + type: oci + url: oci://ghcr.io/stefanprodan/charts + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + namespace: podinfo + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + chart: + spec: + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + install: + remediation: + retries: 3 + interval: 50m + releaseName: podinfo + valuesFrom: + - kind: ConfigMap + name: podinfo-values + - kind: Secret + name: podinfo-tls-values + optional: true + targetPath: tls.crt + valuesKey: crt + --- + apiVersion: v1 + data: + values.yaml: |- + redis: + enabled: true + repository: public.ecr.aws/docker/library/redis + tag: 7.0.6 + ingress: + enabled: true + className: nginx + hosts: + - host: podinfo.production + paths: + - path: / + pathType: ImplementationSpecific + kind: ConfigMap + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo-values + namespace: podinfo + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + --- + apiVersion: v1 + data: + crt: dmFsdWUtMg0KDQo= + kind: Secret + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo-tls-values + annotations: + config.kubernetes.io/index: '5' + internal.config.kubernetes.io/index: '5' + + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: apps + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + dependsOn: + - name: infra-configs + interval: 10m0s + path: ./tests/testdata/cluster8/apps + prune: true + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m0s + wait: true + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + 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: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m0s + path: ./tests/testdata/cluster8/cluster/ + prune: true + sourceRef: + kind: GitRepository + name: flux-system + + --- + # Source: podinfo/templates/redis/config.yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: podinfo-redis + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + data: + redis.conf: | + maxmemory 64mb + maxmemory-policy allkeys-lru + save "" + appendonly no + --- + # Source: podinfo/templates/redis/service.yaml + apiVersion: v1 + kind: Service + metadata: + name: podinfo-redis + labels: + app: podinfo-redis + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + type: ClusterIP + selector: + app: podinfo-redis + ports: + - name: redis + port: 6379 + protocol: TCP + targetPort: redis + --- + # Source: podinfo/templates/service.yaml + apiVersion: v1 + kind: Service + metadata: + name: podinfo + labels: + helm.sh/chart: podinfo-6.5.4 + app.kubernetes.io/name: podinfo + app.kubernetes.io/version: "6.5.4" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + type: ClusterIP + ports: + - port: 9898 + targetPort: http + protocol: TCP + name: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc + selector: + app.kubernetes.io/name: podinfo + --- + # Source: podinfo/templates/deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: podinfo + labels: + helm.sh/chart: podinfo-6.5.4 + app.kubernetes.io/name: podinfo + app.kubernetes.io/version: "6.5.4" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: podinfo + template: + metadata: + labels: + app.kubernetes.io/name: podinfo + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9898" + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: podinfo + image: "ghcr.io/stefanprodan/podinfo:6.5.4" + imagePullPolicy: IfNotPresent + command: + - ./podinfo + - --port=9898 + - --cert-path=/data/cert + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --cache-server=tcp://podinfo-redis:6379 + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 1 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + periodSeconds: 10 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 1 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + periodSeconds: 10 + volumeMounts: + - name: data + mountPath: /data + resources: + limits: null + requests: + cpu: 1m + memory: 16Mi + volumes: + - name: data + emptyDir: {} + --- + # Source: podinfo/templates/redis/deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: podinfo-redis + labels: + app: podinfo-redis + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + strategy: + type: Recreate + selector: + matchLabels: + app: podinfo-redis + template: + metadata: + labels: + app: podinfo-redis + annotations: + checksum/config: "34c601c9d39797bbf53d1c7a278976609301f637ec11dc0253563729dfad4f8e" + spec: + containers: + - name: redis + image: "public.ecr.aws/docker/library/redis:7.0.6" + imagePullPolicy: IfNotPresent + command: + - redis-server + - "/redis-master/redis.conf" + ports: + - name: redis + containerPort: 6379 + protocol: TCP + livenessProbe: + tcpSocket: + port: redis + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi + volumeMounts: + - mountPath: /var/lib/redis + name: data + - mountPath: /redis-master + name: config + volumes: + - name: data + emptyDir: {} + - name: config + configMap: + name: podinfo-redis + items: + - key: redis.conf + path: redis.conf + --- + # Source: podinfo/templates/ingress.yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: podinfo + labels: + helm.sh/chart: podinfo-6.5.4 + app.kubernetes.io/name: podinfo + app.kubernetes.io/version: "6.5.4" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '5' + internal.config.kubernetes.io/index: '5' + spec: + ingressClassName: nginx + rules: + - host: "podinfo.production" + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: podinfo + port: + number: 9898 + + + ''' +# --- +# name: test_build[build-helm-cluster] + ''' + --- + apiVersion: v1 + kind: Namespace + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + namespace: podinfo + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + chart: + spec: + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + version: 6.3.2 + install: + remediation: + retries: 3 + interval: 50m + releaseName: podinfo + values: + ingress: + className: nginx + enabled: true + hosts: + - host: podinfo.production + paths: + - path: / + pathType: ImplementationSpecific + redis: + enabled: true + repository: public.ecr.aws/docker/library/redis + tag: 7.0.6 + --- + apiVersion: v1 + data: + foo: bar + kind: ConfigMap + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo-config + namespace: podinfo + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: apps + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + dependsOn: + - name: infra-configs + interval: 10m0s + path: ./tests/testdata/cluster/apps/prod + prune: true + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m0s + wait: true + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + 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: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m0s + path: ./tests/testdata/cluster/clusters/prod + prune: true + sourceRef: + kind: GitRepository + name: flux-system + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: infra-controllers + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + interval: 1h + path: ./tests/testdata/cluster/infrastructure/controllers + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m + wait: true + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: infra-configs + namespace: flux-system + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + dependsOn: + - name: infra-controllers + interval: 1h + path: ./tests/testdata/cluster/infrastructure/configs + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m + + --- + apiVersion: kyverno.io/v1 + kind: ClusterPolicy + metadata: + annotations: + policies.kyverno.io/description: Policy that is expected to allow resources under test through since no resources should have this annotation. + policies.kyverno.io/title: Test Allow Policy + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: test-allow-policy + spec: + background: true + rules: + - match: + resources: + kinds: + - ConfigMap + name: forbid-test-annotation + validate: + message: Found test-annotation + pattern: + metadata: + =(annotations): + X(flux-local/test-annotation): "null" + validationFailureAction: audit + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: bitnami + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + interval: 30m + provider: generic + timeout: 1m0s + url: https://charts.bitnami.com/bitnami + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 5m + type: oci + url: oci://ghcr.io/stefanprodan/charts + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: weave-charts + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + interval: 120m + type: oci + url: oci://ghcr.io/weaveworks/charts + + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-controllers + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: metallb + namespace: metallb + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + chart: + spec: + chart: metallb + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: bitnami + namespace: flux-system + version: 4.1.14 + install: + crds: CreateReplace + remediation: + retries: 3 + interval: 5m + releaseName: metallb + upgrade: + crds: CreateReplace + values: + speaker: + secretName: metallb-secret + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-controllers + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: weave-gitops + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + chart: + spec: + chart: weave-gitops + interval: 12h + sourceRef: + kind: HelmRepository + name: weave-charts + version: 4.0.22 + interval: 60m + + --- + # Source: weave-gitops/templates/network-policy.yaml + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-dashboard-ingress + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + ingress: + - ports: + - port: 9001 + protocol: TCP + policyTypes: + - Ingress + --- + # Source: weave-gitops/templates/network-policy.yaml + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-dashboard-egress + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + egress: + - {} + policyTypes: + - Egress + --- + # Source: weave-gitops/templates/serviceaccount.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: weave-gitops + labels: + helm.sh/chart: weave-gitops-4.0.22 + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + app.kubernetes.io/version: "v0.24.0" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + --- + # Source: weave-gitops/templates/role.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: weave-gitops + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + rules: + # impersonation rules for ui calls + - apiGroups: [""] + resources: ["users", "groups"] + verbs: ["impersonate"] + # Access to enterprise entitlement + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + # or should return the first non-falsy result + resourceNames: ["cluster-user-auth", "oidc-auth"] + # The service account needs to read namespaces to know where it can query + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch"] + # The service account needs to list custom resources to query if given feature + # is available or not. + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["list"] + --- + # Source: weave-gitops/templates/rolebinding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: weave-gitops + labels: + helm.sh/chart: weave-gitops-4.0.22 + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + app.kubernetes.io/version: "v0.24.0" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + subjects: + - kind: ServiceAccount + name: weave-gitops + namespace: flux-system + roleRef: + kind: ClusterRole + name: weave-gitops + apiGroup: rbac.authorization.k8s.io + --- + # Source: weave-gitops/templates/service.yaml + apiVersion: v1 + kind: Service + metadata: + name: weave-gitops + labels: + helm.sh/chart: weave-gitops-4.0.22 + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + app.kubernetes.io/version: "v0.24.0" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '5' + internal.config.kubernetes.io/index: '5' + spec: + type: ClusterIP + ports: + - port: 9001 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + --- + # Source: weave-gitops/templates/deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: weave-gitops + labels: + helm.sh/chart: weave-gitops-4.0.22 + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + app.kubernetes.io/version: "v0.24.0" + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/part-of: weave-gitops + weave.works/app: weave-gitops-oss + annotations: + config.kubernetes.io/index: '6' + internal.config.kubernetes.io/index: '6' + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + template: + metadata: + labels: + app.kubernetes.io/name: weave-gitops + app.kubernetes.io/instance: weave-gitops + app.kubernetes.io/part-of: weave-gitops + weave.works/app: weave-gitops-oss + spec: + serviceAccountName: weave-gitops + securityContext: {} + containers: + - name: weave-gitops + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + image: "ghcr.io/weaveworks/wego-app:v0.24.0" + imagePullPolicy: IfNotPresent + args: + - "--log-level" + - "info" + - "--insecure" + ports: + - name: http + containerPort: 9001 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + env: + - name: WEAVE_GITOPS_FEATURE_TENANCY + value: "true" + - name: WEAVE_GITOPS_FEATURE_CLUSTER + value: "false" + resources: {} + + --- + # Source: metallb/templates/controller/serviceaccount.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: metallb-controller + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + automountServiceAccountToken: true + --- + # Source: metallb/templates/speaker/serviceaccount.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: metallb-speaker + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + automountServiceAccountToken: true + --- + # Source: metallb/templates/controller/configmap.yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: metallb-config + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + data: + config: | + null + --- + # Source: metallb/templates/controller/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: metallb:controller + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + annotations: + config.kubernetes.io/index: '11' + internal.config.kubernetes.io/index: '11' + rules: + - apiGroups: + - '' + resources: + - services + verbs: + - get + - list + - watch + - update + - apiGroups: + - '' + resources: + - services/status + verbs: + - update + - apiGroups: + - '' + resources: + - events + verbs: + - create + - patch + - apiGroups: + - policy + resourceNames: + - metallb-controller + resources: + - podsecuritypolicies + verbs: + - use + - apiGroups: + - admissionregistration.k8s.io + resources: + - validatingwebhookconfigurations + - mutatingwebhookconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + --- + # Source: metallb/templates/speaker/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: metallb-speaker + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + annotations: + config.kubernetes.io/index: '12' + internal.config.kubernetes.io/index: '12' + rules: + - apiGroups: + - '' + resources: + - services + - endpoints + - nodes + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - events + verbs: + - create + - patch + - apiGroups: + - policy + resourceNames: + - metallb-speaker + resources: + - podsecuritypolicies + verbs: + - use + - apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch + --- + # Source: metallb/templates/controller/rbac.yaml + ## Role bindings + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: metallb:controller + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + annotations: + config.kubernetes.io/index: '13' + internal.config.kubernetes.io/index: '13' + subjects: + - kind: ServiceAccount + name: metallb-controller + namespace: metallb + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metallb:controller + --- + # Source: metallb/templates/speaker/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: metallb-speaker + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + annotations: + config.kubernetes.io/index: '14' + internal.config.kubernetes.io/index: '14' + subjects: + - kind: ServiceAccount + name: metallb-speaker + namespace: "metallb" + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metallb-speaker + --- + # Source: metallb/templates/controller/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: metallb-controller + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + annotations: + config.kubernetes.io/index: '15' + internal.config.kubernetes.io/index: '15' + rules: + - apiGroups: + - '' + resources: + - secrets + verbs: + - create + - get + - list + - watch + - apiGroups: + - '' + resources: + - secrets + resourceNames: + - "metallb-secret" + verbs: + - list + - apiGroups: + - apps + resources: + - deployments + resourceNames: + - metallb-controller + verbs: + - get + - apiGroups: + - '' + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - metallb.io + resources: + - addresspools + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - ipaddresspools + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - bgppeers + verbs: + - get + - list + - apiGroups: + - metallb.io + resources: + - bgpadvertisements + verbs: + - get + - list + - apiGroups: + - metallb.io + resources: + - l2advertisements + verbs: + - get + - list + - apiGroups: + - metallb.io + resources: + - communities + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - bfdprofiles + verbs: + - get + - list + - watch + --- + # Source: metallb/templates/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: metallb-config-watcher + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '16' + internal.config.kubernetes.io/index: '16' + rules: + - apiGroups: + - '' + resources: + - configmaps + verbs: + - get + - list + - watch + --- + # Source: metallb/templates/speaker/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: metallb-pod-lister + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + annotations: + config.kubernetes.io/index: '17' + internal.config.kubernetes.io/index: '17' + rules: + - apiGroups: + - '' + resources: + - pods + verbs: + - list + - apiGroups: + - '' + resources: + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - addresspools + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - bfdprofiles + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - bgppeers + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - l2advertisements + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - bgpadvertisements + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - ipaddresspools + verbs: + - get + - list + - watch + - apiGroups: + - metallb.io + resources: + - communities + verbs: + - get + - list + - watch + --- + # Source: metallb/templates/controller/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: metallb-controller + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + annotations: + config.kubernetes.io/index: '18' + internal.config.kubernetes.io/index: '18' + subjects: + - kind: ServiceAccount + name: metallb-controller + namespace: metallb + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: metallb-controller + --- + # Source: metallb/templates/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: metallb-config-watcher + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '19' + internal.config.kubernetes.io/index: '19' + subjects: + - kind: ServiceAccount + name: metallb-controller + - kind: ServiceAccount + name: metallb-speaker + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: metallb-config-watcher + --- + # Source: metallb/templates/speaker/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: metallb-pod-lister + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + annotations: + config.kubernetes.io/index: '20' + internal.config.kubernetes.io/index: '20' + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: metallb-pod-lister + subjects: + - kind: ServiceAccount + name: metallb-speaker + --- + # Source: metallb/templates/controller/webhooks.yaml + apiVersion: v1 + kind: Service + metadata: + name: metallb-webhook-service + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '21' + internal.config.kubernetes.io/index: '21' + spec: + ports: + - port: 443 + targetPort: 9443 + selector: + app.kubernetes.io/name: metallb + app.kubernetes.io/instance: metallb + app.kubernetes.io/component: controller + --- + # Source: metallb/templates/speaker/daemonset.yaml + apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: metallb-speaker + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + annotations: + config.kubernetes.io/index: '22' + internal.config.kubernetes.io/index: '22' + spec: + updateStrategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: metallb + app.kubernetes.io/instance: metallb + app.kubernetes.io/component: speaker + template: + metadata: + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: speaker + spec: + serviceAccountName: metallb-speaker + affinity: + podAffinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: metallb + app.kubernetes.io/instance: metallb + app.kubernetes.io/component: speaker + topologyKey: kubernetes.io/hostname + weight: 1 + nodeAffinity: + nodeSelector: + "kubernetes.io/os": linux + hostNetwork: true + securityContext: + fsGroup: 0 + terminationGracePeriodSeconds: 2 + containers: + - name: metallb-speaker + image: docker.io/bitnami/metallb-speaker:0.13.7-debian-11-r28 + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_ADMIN + - NET_RAW + - SYS_ADMIN + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 0 + args: + - --port=7472 + env: + - name: METALLB_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: METALLB_HOST + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: METALLB_ML_BIND_ADDR + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: METALLB_ML_LABELS + value: "app.kubernetes.io/name=metallb,app.kubernetes.io/instance=metallb,app.kubernetes.io/component=speaker" + - name: METALLB_ML_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: METALLB_ML_SECRET_KEY + valueFrom: + secretKeyRef: + name: metallb-secret + key: secretkey + envFrom: + ports: + - name: metrics + containerPort: 7472 + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + httpGet: + path: /metrics + port: metrics + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + httpGet: + path: /metrics + port: metrics + resources: + limits: {} + requests: {} + --- + # Source: metallb/templates/controller/deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: metallb-controller + namespace: "metallb" + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + annotations: + config.kubernetes.io/index: '23' + internal.config.kubernetes.io/index: '23' + spec: + replicas: 1 + strategy: + type: RollingUpdate + revisionHistoryLimit: 3 + selector: + matchLabels: + app.kubernetes.io/name: metallb + app.kubernetes.io/instance: metallb + app.kubernetes.io/component: controller + template: + metadata: + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + spec: + serviceAccountName: metallb-controller + nodeSelector: + "kubernetes.io/os": linux + affinity: + podAffinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: metallb + app.kubernetes.io/instance: metallb + app.kubernetes.io/component: controller + topologyKey: kubernetes.io/hostname + weight: 1 + nodeAffinity: + securityContext: + fsGroup: 1001 + containers: + - name: metallb-controller + image: docker.io/bitnami/metallb-controller:0.13.7-debian-11-r29 + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1001 + args: + - --port=7472 + - --cert-service-name=metallb-webhook-service + env: + envFrom: + ports: + - name: metrics + containerPort: 7472 + - name: webhook-server + containerPort: 9443 + protocol: TCP + volumeMounts: + - name: cert + mountPath: /tmp/k8s-webhook-server/serving-certs + readOnly: true + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + httpGet: + path: /metrics + port: metrics + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + httpGet: + path: /metrics + port: metrics + resources: + limits: {} + requests: {} + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert + --- + # Source: metallb/templates/controller/webhooks.yaml + apiVersion: admissionregistration.k8s.io/v1 + kind: ValidatingWebhookConfiguration + metadata: + name: metallb-webhook-configuration + labels: + app.kubernetes.io/name: metallb + helm.sh/chart: metallb-4.1.14 + app.kubernetes.io/instance: metallb + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '24' + internal.config.kubernetes.io/index: '24' + webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta1-addresspool + failurePolicy: Fail + name: addresspoolvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - addresspools + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta2-bgppeer + failurePolicy: Fail + name: bgppeervalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - bgppeers + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta1-ipaddresspool + failurePolicy: Fail + name: ipaddresspoolvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - ipaddresspools + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta1-bgpadvertisement + failurePolicy: Fail + name: bgpadvertisementvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - bgpadvertisements + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta1-community + failurePolicy: Fail + name: communityvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - communities + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta1-bfdprofile + failurePolicy: Fail + name: bfdprofileyvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - DELETE + resources: + - bfdprofiles + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metallb-webhook-service + namespace: "metallb" + path: /validate-metallb-io-v1beta1-l2advertisement + failurePolicy: Fail + name: l2advertisementvalidationwebhook.metallb.io + rules: + - apiGroups: + - metallb.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - l2advertisements + sideEffects: None + + --- + # Source: podinfo/templates/redis/config.yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: podinfo-redis + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + data: + redis.conf: | + maxmemory 64mb + maxmemory-policy allkeys-lru + save "" + appendonly no + --- + # Source: podinfo/templates/redis/service.yaml + apiVersion: v1 + kind: Service + metadata: + name: podinfo-redis + labels: + app: podinfo-redis + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + type: ClusterIP + selector: + app: podinfo-redis + ports: + - name: redis + port: 6379 + protocol: TCP + targetPort: redis + --- + # Source: podinfo/templates/service.yaml + apiVersion: v1 + kind: Service + metadata: + name: podinfo + labels: + helm.sh/chart: podinfo-6.3.2 + app.kubernetes.io/name: podinfo + app.kubernetes.io/version: "6.3.2" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + type: ClusterIP + ports: + - port: 9898 + targetPort: http + protocol: TCP + name: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc + selector: + app.kubernetes.io/name: podinfo + --- + # Source: podinfo/templates/deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: podinfo + labels: + helm.sh/chart: podinfo-6.3.2 + app.kubernetes.io/name: podinfo + app.kubernetes.io/version: "6.3.2" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: podinfo + template: + metadata: + labels: + app.kubernetes.io/name: podinfo + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9898" + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: podinfo + image: "ghcr.io/stefanprodan/podinfo:6.3.2" + imagePullPolicy: IfNotPresent + command: + - ./podinfo + - --port=9898 + - --cert-path=/data/cert + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --cache-server=tcp://podinfo-redis:6379 + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 1 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + periodSeconds: 10 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 1 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 + periodSeconds: 10 + volumeMounts: + - name: data + mountPath: /data + resources: + limits: null + requests: + cpu: 1m + memory: 16Mi + volumes: + - name: data + emptyDir: {} + --- + # Source: podinfo/templates/redis/deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: podinfo-redis + labels: + app: podinfo-redis + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + strategy: + type: Recreate + selector: + matchLabels: + app: podinfo-redis + template: + metadata: + labels: + app: podinfo-redis + annotations: + checksum/config: "34c601c9d39797bbf53d1c7a278976609301f637ec11dc0253563729dfad4f8e" + spec: + containers: + - name: redis + image: "public.ecr.aws/docker/library/redis:7.0.6" + imagePullPolicy: IfNotPresent + command: + - redis-server + - "/redis-master/redis.conf" + ports: + - name: redis + containerPort: 6379 + protocol: TCP + livenessProbe: + tcpSocket: + port: redis + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi + volumeMounts: + - mountPath: /var/lib/redis + name: data + - mountPath: /redis-master + name: config + volumes: + - name: data + emptyDir: {} + - name: config + configMap: + name: podinfo-redis + items: + - key: redis.conf + path: redis.conf + --- + # Source: podinfo/templates/ingress.yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: podinfo + labels: + helm.sh/chart: podinfo-6.3.2 + app.kubernetes.io/name: podinfo + app.kubernetes.io/version: "6.3.2" + app.kubernetes.io/managed-by: Helm + annotations: + config.kubernetes.io/index: '5' + internal.config.kubernetes.io/index: '5' + spec: + ingressClassName: nginx + rules: + - host: "podinfo.production" + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: podinfo + port: + number: 9898 + + + ''' +# --- +# name: test_build[build] + ''' + --- + apiVersion: v1 + kind: Namespace + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + namespace: podinfo + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + chart: + spec: + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + version: 6.3.2 + install: + remediation: + retries: 3 + interval: 50m + releaseName: podinfo + values: + ingress: + className: nginx + enabled: true + hosts: + - host: podinfo.production + paths: + - path: / + pathType: ImplementationSpecific + redis: + enabled: true + repository: public.ecr.aws/docker/library/redis + tag: 7.0.6 + --- + apiVersion: v1 + data: + foo: bar + kind: ConfigMap + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: apps + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo-config + namespace: podinfo + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: apps + namespace: flux-system + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + dependsOn: + - name: infra-configs + interval: 10m0s + path: ./tests/testdata/cluster/apps/prod + prune: true + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m0s + wait: true + --- + apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + 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: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: flux-system + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 10m0s + path: ./tests/testdata/cluster/clusters/prod + prune: true + sourceRef: + kind: GitRepository + name: flux-system + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: infra-controllers + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + interval: 1h + path: ./tests/testdata/cluster/infrastructure/controllers + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m + wait: true + --- + apiVersion: kustomize.toolkit.fluxcd.io/v1 + kind: Kustomization + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: infra-configs + namespace: flux-system + annotations: + config.kubernetes.io/index: '4' + internal.config.kubernetes.io/index: '4' + spec: + dependsOn: + - name: infra-controllers + interval: 1h + path: ./tests/testdata/cluster/infrastructure/configs + prune: true + retryInterval: 1m + sourceRef: + kind: GitRepository + name: flux-system + timeout: 5m + + --- + apiVersion: kyverno.io/v1 + kind: ClusterPolicy + metadata: + annotations: + policies.kyverno.io/description: Policy that is expected to allow resources under test through since no resources should have this annotation. + policies.kyverno.io/title: Test Allow Policy + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: test-allow-policy + spec: + background: true + rules: + - match: + resources: + kinds: + - ConfigMap + name: forbid-test-annotation + validate: + message: Found test-annotation + pattern: + metadata: + =(annotations): + X(flux-local/test-annotation): "null" + validationFailureAction: audit + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: bitnami + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + interval: 30m + provider: generic + timeout: 1m0s + url: https://charts.bitnami.com/bitnami + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: podinfo + namespace: flux-system + annotations: + config.kubernetes.io/index: '2' + internal.config.kubernetes.io/index: '2' + spec: + interval: 5m + type: oci + url: oci://ghcr.io/stefanprodan/charts + --- + apiVersion: source.toolkit.fluxcd.io/v1beta2 + kind: HelmRepository + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-configs + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: weave-charts + namespace: flux-system + annotations: + config.kubernetes.io/index: '3' + internal.config.kubernetes.io/index: '3' + spec: + interval: 120m + type: oci + url: oci://ghcr.io/weaveworks/charts + + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-controllers + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: metallb + namespace: metallb + annotations: + config.kubernetes.io/index: '0' + internal.config.kubernetes.io/index: '0' + spec: + chart: + spec: + chart: metallb + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: bitnami + namespace: flux-system + version: 4.1.14 + install: + crds: CreateReplace + remediation: + retries: 3 + interval: 5m + releaseName: metallb + upgrade: + crds: CreateReplace + values: + speaker: + secretName: metallb-secret + --- + apiVersion: helm.toolkit.fluxcd.io/v2beta1 + kind: HelmRelease + metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infra-controllers + kustomize.toolkit.fluxcd.io/namespace: flux-system + name: weave-gitops + namespace: flux-system + annotations: + config.kubernetes.io/index: '1' + internal.config.kubernetes.io/index: '1' + spec: + chart: + spec: + chart: weave-gitops + interval: 12h + sourceRef: + kind: HelmRepository + name: weave-charts + version: 4.0.22 + interval: 60m + + + ''' +# --- diff --git a/tests/tool/__snapshots__/test_diagnostics.ambr b/tests/tool/__snapshots__/test_diagnostics.ambr new file mode 100644 index 00000000..8e5bb359 --- /dev/null +++ b/tests/tool/__snapshots__/test_diagnostics.ambr @@ -0,0 +1,27 @@ +# serializer version: 1 +# name: test_diagnostics + ''' + [DIAGNOSTICS OK] + + ''' +# --- +# name: test_invalid_mapping[list] + ''' + [DIAGNOSTICS FAIL]: `<>` expected dictionary but was list: ['entry'] + + ''' +# --- +# name: test_invalid_mapping[not-yaml] + ''' + [DIAGNOSTICS FAIL]: `<>` failed to parse as yaml: unacceptable character #x0000: special characters are not allowed + in "", position 0 + + ''' +# --- +# name: test_invalid_mapping[scalar] + ''' + [DIAGNOSTICS FAIL]: `<>` was not a dictionary: : entry + [DIAGNOSTICS FAIL]: `<>` was not a dictionary: : var + + ''' +# --- diff --git a/tests/tool/__snapshots__/test_diff.ambr b/tests/tool/__snapshots__/test_diff.ambr new file mode 100644 index 00000000..4b35fae4 --- /dev/null +++ b/tests/tool/__snapshots__/test_diff.ambr @@ -0,0 +1,24 @@ +# serializer version: 1 +# name: test_diff_ks + ''' + --- tests/testdata/cluster/apps/prod Kustomization: flux-system/apps ConfigMap: podinfo/podinfo-config + + +++ tests/testdata/cluster/apps/prod Kustomization: flux-system/apps ConfigMap: podinfo/podinfo-config + + @@ -1,12 +0,0 @@ + + ---- + -apiVersion: v1 + -data: + - foo: bar + -kind: ConfigMap + -metadata: + - labels: + - kustomize.toolkit.fluxcd.io/name: apps + - kustomize.toolkit.fluxcd.io/namespace: flux-system + - name: podinfo-config + - namespace: podinfo + - + + ''' +# --- diff --git a/tests/tool/__snapshots__/test_diff_hr.ambr b/tests/tool/__snapshots__/test_diff_hr.ambr new file mode 100644 index 00000000..d36d6c11 --- /dev/null +++ b/tests/tool/__snapshots__/test_diff_hr.ambr @@ -0,0 +1,43 @@ +# serializer version: 1 +# name: test_diff_hr[all-namespaces] + '' +# --- +# name: test_diff_hr[all] + '' +# --- +# name: test_diff_hr[cluster6] + '' +# --- +# name: test_diff_hr[external-limit-bytes] + '' +# --- +# name: test_diff_hr[external] + '' +# --- +# name: test_diff_hr[limit-bytes] + '' +# --- +# name: test_diff_hr[not-found-all-namespaces] + ''' + HelmRelease object 'example' not found in 'None' namespace + + ''' +# --- +# name: test_diff_hr[not-found-namespace] + ''' + HelmRelease object 'example' not found in 'example' namespace + + ''' +# --- +# name: test_diff_hr[not-found] + ''' + HelmRelease object 'example' not found in 'flux-system' namespace + + ''' +# --- +# name: test_diff_hr[podinfo] + '' +# --- +# name: test_diff_hr[yaml] + '' +# --- diff --git a/tests/tool/__snapshots__/test_diff_ks.ambr b/tests/tool/__snapshots__/test_diff_ks.ambr new file mode 100644 index 00000000..33bd579a --- /dev/null +++ b/tests/tool/__snapshots__/test_diff_ks.ambr @@ -0,0 +1,116 @@ +# serializer version: 1 +# name: test_diff_ks[apps] + '' +# --- +# name: test_diff_ks[ks-external] + '' +# --- +# name: test_diff_ks[yaml-empty-sources] + ''' + --- + - kustomization_path: tests/testdata/cluster/apps/prod + kind: Kustomization + namespace: flux-system + name: apps + diffs: + - kustomization_path: tests/testdata/cluster/apps/prod + kind: Namespace + namespace: flux-system + name: podinfo + diff: |- + --- tests/testdata/cluster/apps/prod Kustomization: flux-system/apps Namespace: flux-system/podinfo + + +++ tests/testdata/cluster/apps/prod Kustomization: flux-system/apps Namespace: flux-system/podinfo + + @@ -1,9 +0,0 @@ + + ---- + -apiVersion: v1 + -kind: Namespace + -metadata: + - labels: + - kustomize.toolkit.fluxcd.io/name: apps + - kustomize.toolkit.fluxcd.io/namespace: flux-system + - name: podinfo + - + - kustomization_path: tests/testdata/cluster/apps/prod + kind: HelmRelease + namespace: podinfo + name: podinfo + diff: |- + --- tests/testdata/cluster/apps/prod Kustomization: flux-system/apps HelmRelease: podinfo/podinfo + + +++ tests/testdata/cluster/apps/prod Kustomization: flux-system/apps HelmRelease: podinfo/podinfo + + @@ -1,37 +0,0 @@ + + ---- + -apiVersion: helm.toolkit.fluxcd.io/v2beta1 + -kind: HelmRelease + -metadata: + - labels: + - kustomize.toolkit.fluxcd.io/name: apps + - kustomize.toolkit.fluxcd.io/namespace: flux-system + - name: podinfo + - namespace: podinfo + -spec: + - chart: + - spec: + - chart: podinfo + - sourceRef: + - kind: HelmRepository + - name: podinfo + - namespace: flux-system + - version: 6.3.2 + - install: + - remediation: + - retries: 3 + - interval: 50m + - releaseName: podinfo + - values: + - ingress: + - className: nginx + - enabled: true + - hosts: + - - host: podinfo.production + - paths: + - - path: / + - pathType: ImplementationSpecific + - redis: + - enabled: true + - repository: public.ecr.aws/docker/library/redis + - tag: 7.0.6 + - + - kustomization_path: tests/testdata/cluster/apps/prod + kind: ConfigMap + namespace: podinfo + name: podinfo-config + diff: |- + --- tests/testdata/cluster/apps/prod Kustomization: flux-system/apps ConfigMap: podinfo/podinfo-config + + +++ tests/testdata/cluster/apps/prod Kustomization: flux-system/apps ConfigMap: podinfo/podinfo-config + + @@ -1,12 +0,0 @@ + + ---- + -apiVersion: v1 + -data: + - foo: bar + -kind: ConfigMap + -metadata: + - labels: + - kustomize.toolkit.fluxcd.io/name: apps + - kustomize.toolkit.fluxcd.io/namespace: flux-system + - name: podinfo-config + - namespace: podinfo + - + + + ''' +# --- +# name: test_diff_ks[yaml-limit] + '' +# --- +# name: test_diff_ks[yaml] + '' +# --- diff --git a/tests/tool/__snapshots__/test_get_cluster.ambr b/tests/tool/__snapshots__/test_get_cluster.ambr new file mode 100644 index 00000000..f24341c7 --- /dev/null +++ b/tests/tool/__snapshots__/test_get_cluster.ambr @@ -0,0 +1,344 @@ +# serializer version: 1 +# name: test_get_cluster[all-namespaces] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster 4 + + ''' +# --- +# name: test_get_cluster[cluster2] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster2 5 + + ''' +# --- +# name: test_get_cluster[cluster3-no-source] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster3 1 + + ''' +# --- +# name: test_get_cluster[cluster3] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster3 2 + + ''' +# --- +# name: test_get_cluster[cluster4] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster4 3 + + ''' +# --- +# name: test_get_cluster[cluster5] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster5 1 + + ''' +# --- +# name: test_get_cluster[cluster6] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster6 2 + + ''' +# --- +# name: test_get_cluster[cluster7] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster7 3 + + ''' +# --- +# name: test_get_cluster[cluster] + ''' + PATH KUSTOMIZATIONS + tests/testdata/cluster 4 + + ''' +# --- +# name: test_get_cluster[yaml-cluster-images] + ''' + --- + clusters: + - path: tests/testdata/cluster + kustomizations: + - name: apps + namespace: flux-system + path: tests/testdata/cluster/apps/prod + helm_repos: [] + helm_releases: + - name: podinfo + namespace: podinfo + chart: + name: podinfo + repo_name: podinfo + repo_namespace: flux-system + images: + - ghcr.io/stefanprodan/podinfo:6.3.2 + - public.ecr.aws/docker/library/redis:7.0.6 + cluster_policies: [] + config_maps: + - name: podinfo-config + namespace: podinfo + secrets: [] + - name: flux-system + namespace: flux-system + path: tests/testdata/cluster/clusters/prod + helm_repos: [] + helm_releases: [] + cluster_policies: [] + config_maps: [] + secrets: [] + - name: infra-configs + namespace: flux-system + path: tests/testdata/cluster/infrastructure/configs + helm_repos: + - name: bitnami + namespace: flux-system + url: https://charts.bitnami.com/bitnami + repo_type: default + - name: podinfo + namespace: flux-system + url: oci://ghcr.io/stefanprodan/charts + repo_type: oci + - name: weave-charts + namespace: flux-system + url: oci://ghcr.io/weaveworks/charts + repo_type: oci + helm_releases: [] + cluster_policies: + - name: test-allow-policy + config_maps: [] + secrets: [] + - name: infra-controllers + namespace: flux-system + path: tests/testdata/cluster/infrastructure/controllers + helm_repos: [] + helm_releases: + - name: metallb + namespace: metallb + chart: + name: metallb + repo_name: bitnami + repo_namespace: flux-system + images: + - docker.io/bitnami/metallb-controller:0.13.7-debian-11-r29 + - docker.io/bitnami/metallb-speaker:0.13.7-debian-11-r28 + - name: weave-gitops + namespace: flux-system + chart: + name: weave-gitops + repo_name: weave-charts + repo_namespace: flux-system + images: + - ghcr.io/weaveworks/wego-app:v0.24.0 + cluster_policies: [] + config_maps: [] + secrets: [] + + ''' +# --- +# name: test_get_cluster[yaml-cluster-no-images] + ''' + --- + clusters: + - path: tests/testdata/cluster + kustomizations: + - name: apps + namespace: flux-system + path: tests/testdata/cluster/apps/prod + helm_repos: [] + helm_releases: + - name: podinfo + namespace: podinfo + chart: + name: podinfo + repo_name: podinfo + repo_namespace: flux-system + cluster_policies: [] + config_maps: + - name: podinfo-config + namespace: podinfo + secrets: [] + - name: flux-system + namespace: flux-system + path: tests/testdata/cluster/clusters/prod + helm_repos: [] + helm_releases: [] + cluster_policies: [] + config_maps: [] + secrets: [] + - name: infra-configs + namespace: flux-system + path: tests/testdata/cluster/infrastructure/configs + helm_repos: + - name: bitnami + namespace: flux-system + url: https://charts.bitnami.com/bitnami + repo_type: default + - name: podinfo + namespace: flux-system + url: oci://ghcr.io/stefanprodan/charts + repo_type: oci + - name: weave-charts + namespace: flux-system + url: oci://ghcr.io/weaveworks/charts + repo_type: oci + helm_releases: [] + cluster_policies: + - name: test-allow-policy + config_maps: [] + secrets: [] + - name: infra-controllers + namespace: flux-system + path: tests/testdata/cluster/infrastructure/controllers + helm_repos: [] + helm_releases: + - name: metallb + namespace: metallb + chart: + name: metallb + repo_name: bitnami + repo_namespace: flux-system + - name: weave-gitops + namespace: flux-system + chart: + name: weave-gitops + repo_name: weave-charts + repo_namespace: flux-system + cluster_policies: [] + config_maps: [] + secrets: [] + + ''' +# --- +# name: test_get_cluster[yaml-cluster8-images-allow-secrets] + ''' + --- + clusters: + - path: tests/testdata/cluster8 + kustomizations: + - name: apps + namespace: flux-system + path: tests/testdata/cluster8/apps + helm_repos: + - name: podinfo + namespace: flux-system + url: oci://ghcr.io/stefanprodan/charts + repo_type: oci + helm_releases: + - name: podinfo + namespace: podinfo + chart: + name: podinfo + repo_name: podinfo + repo_namespace: flux-system + images: + - ghcr.io/stefanprodan/podinfo:6.5.4 + - public.ecr.aws/docker/library/redis:7.0.6 + cluster_policies: [] + config_maps: + - name: podinfo-values + namespace: podinfo + secrets: + - name: podinfo-tls-values + images: + - alpine + - busybox + - name: flux-system + namespace: flux-system + path: tests/testdata/cluster8/cluster + helm_repos: [] + helm_releases: [] + cluster_policies: [] + config_maps: [] + secrets: [] + + ''' +# --- +# name: test_get_cluster[yaml-cluster8-images] + ''' + --- + clusters: + - path: tests/testdata/cluster8 + kustomizations: + - name: apps + namespace: flux-system + path: tests/testdata/cluster8/apps + helm_repos: + - name: podinfo + namespace: flux-system + url: oci://ghcr.io/stefanprodan/charts + repo_type: oci + helm_releases: + - name: podinfo + namespace: podinfo + chart: + name: podinfo + repo_name: podinfo + repo_namespace: flux-system + images: + - ghcr.io/stefanprodan/podinfo:6.5.4 + - public.ecr.aws/docker/library/redis:7.0.6 + cluster_policies: [] + config_maps: + - name: podinfo-values + namespace: podinfo + secrets: [] + images: + - alpine + - busybox + - name: flux-system + namespace: flux-system + path: tests/testdata/cluster8/cluster + helm_repos: [] + helm_releases: [] + cluster_policies: [] + config_maps: [] + secrets: [] + + ''' +# --- +# name: test_get_cluster[yaml-cluster8-no-images] + ''' + --- + clusters: + - path: tests/testdata/cluster8 + kustomizations: + - name: apps + namespace: flux-system + path: tests/testdata/cluster8/apps + helm_repos: + - name: podinfo + namespace: flux-system + url: oci://ghcr.io/stefanprodan/charts + repo_type: oci + helm_releases: + - name: podinfo + namespace: podinfo + chart: + name: podinfo + repo_name: podinfo + repo_namespace: flux-system + cluster_policies: [] + config_maps: + - name: podinfo-values + namespace: podinfo + secrets: [] + - name: flux-system + namespace: flux-system + path: tests/testdata/cluster8/cluster + helm_repos: [] + helm_releases: [] + cluster_policies: [] + config_maps: [] + secrets: [] + + ''' +# --- diff --git a/tests/tool/__snapshots__/test_get_hr.ambr b/tests/tool/__snapshots__/test_get_hr.ambr new file mode 100644 index 00000000..9a3c9be6 --- /dev/null +++ b/tests/tool/__snapshots__/test_get_hr.ambr @@ -0,0 +1,74 @@ +# serializer version: 1 +# name: test_get_hr[all_namespace] + ''' + NAMESPACE NAME REVISION CHART SOURCE + metallb metallb 4.1.14 metallb-metallb bitnami + + ''' +# --- +# name: test_get_hr[cluster2] + ''' + NAMESPACE NAME REVISION CHART SOURCE + networking ingress-nginx 4.5.2 networking-ingress-nginx ingress-nginx + monitoring kubernetes-dashboard 6.0.0 monitoring-kubernetes-dashboard kubernetes-dashboard + + ''' +# --- +# name: test_get_hr[cluster3-no-source] + ''' + no HelmRelease objects found in cluster + + ''' +# --- +# name: test_get_hr[cluster3] + ''' + NAMESPACE NAME REVISION CHART SOURCE + podinfo podinfo None podinfo-podinfo podinfo + + ''' +# --- +# name: test_get_hr[cluster4] + ''' + NAMESPACE NAME REVISION CHART SOURCE + monitoring kubernetes-dashboard 6.0.0 monitoring-kubernetes-dashboard kubernetes-dashboard + + ''' +# --- +# name: test_get_hr[cluster5] + ''' + NAMESPACE NAME REVISION CHART SOURCE + metallb metallb 4.1.14 metallb-metallb bitnami + flux-system weave-gitops 4.0.22 flux-system-weave-gitops weave-charts + + ''' +# --- +# name: test_get_hr[cluster6] + ''' + NAMESPACE NAME REVISION CHART SOURCE + default renovate 37.64.3 default-renovate renovate + + ''' +# --- +# name: test_get_hr[cluster7] + ''' + NAMESPACE NAME REVISION CHART SOURCE + database postgresql 12.7.1 database-postgresql bitnami-charts + + ''' +# --- +# name: test_get_hr[cluster] + ''' + NAMESPACE NAME REVISION CHART SOURCE + podinfo podinfo 6.3.2 podinfo-podinfo podinfo + metallb metallb 4.1.14 metallb-metallb bitnami + flux-system weave-gitops 4.0.22 flux-system-weave-gitops weave-charts + + ''' +# --- +# name: test_get_hr[name] + ''' + NAME REVISION CHART SOURCE + metallb 4.1.14 metallb-metallb bitnami + + ''' +# --- diff --git a/tests/tool/__snapshots__/test_get_ks.ambr b/tests/tool/__snapshots__/test_get_ks.ambr new file mode 100644 index 00000000..7a05a5b3 --- /dev/null +++ b/tests/tool/__snapshots__/test_get_ks.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_get_ks[cluster2] + ''' + NAME PATH + cluster tests/testdata/cluster2/flux + cluster-apps tests/testdata/cluster2/apps + cluster-apps-ingress-nginx tests/testdata/cluster2/apps/networking/ingress-nginx/app + cluster-apps-ingress-nginx-certificates tests/testdata/cluster2/apps/networking/ingress-nginx/certificates + cluster-apps-kubernetes-dashboard tests/testdata/cluster2/apps/monitoring/kubernetes-dashboard/app + + ''' +# --- +# name: test_get_ks[cluster3] + ''' + NAMESPACE NAME PATH + flux-system namespaces tests/testdata/cluster3/namespaces/overlays/cluster3 + flux-system tenants tests/testdata/cluster3/tenants/overlays/cluster3 + + ''' +# --- +# name: test_get_ks[cluster4] + ''' + NAME PATH + cluster tests/testdata/cluster4/flux + cluster-apps tests/testdata/cluster4/apps + cluster-apps-kubernetes-dashboard tests/testdata/cluster4/apps/monitoring/kubernetes-dashboard + + ''' +# --- +# name: test_get_ks[cluster5-all] + ''' + NAMESPACE NAME PATH + controllers infra-controllers tests/testdata/cluster5 + flux-system flux-system tests/testdata/cluster5/clusters/prod + + ''' +# --- +# name: test_get_ks[cluster5] + ''' + NAME PATH + flux-system tests/testdata/cluster5/clusters/prod + + ''' +# --- +# name: test_get_ks[cluster6] + ''' + NAME PATH + apps tests/testdata/cluster6/apps + flux-system tests/testdata/cluster6/cluster + + ''' +# --- +# name: test_get_ks[cluster7] + ''' + NAME PATH + apps tests/testdata/cluster7/flux/apps + charts tests/testdata/cluster7/flux/charts + flux-system tests/testdata/cluster7/clusters/home + + ''' +# --- +# name: test_get_ks[cluster] + ''' + NAME PATH + apps tests/testdata/cluster/apps/prod + flux-system tests/testdata/cluster/clusters/prod + infra-configs tests/testdata/cluster/infrastructure/configs + infra-controllers tests/testdata/cluster/infrastructure/controllers + + ''' +# --- +# name: test_get_ks[cluster_path] + ''' + NAME PATH + apps tests/testdata/cluster/apps/prod + flux-system tests/testdata/cluster/clusters/prod + infra-configs tests/testdata/cluster/infrastructure/configs + infra-controllers tests/testdata/cluster/infrastructure/controllers + + ''' +# --- +# name: test_get_ks[ks_path] + ''' + no Kustomization objects found in cluster + + ''' +# --- +# name: test_get_ks[wide] + ''' + NAME PATH HELMREPOS RELEASES + apps tests/testdata/cluster/apps/prod 0 1 + flux-system tests/testdata/cluster/clusters/prod 0 0 + infra-configs tests/testdata/cluster/infrastructure/configs 3 0 + infra-controllers tests/testdata/cluster/infrastructure/controllers 0 2 + + ''' +# --- diff --git a/tests/tool/test_build.py b/tests/tool/test_build.py new file mode 100644 index 00000000..4d9f6d52 --- /dev/null +++ b/tests/tool/test_build.py @@ -0,0 +1,42 @@ +"""Tests for the flux-local `build` command.""" + +import pytest + +from syrupy.assertion import SnapshotAssertion + +from . import run_command + + +@pytest.mark.parametrize( + ("args"), + [ + (["tests/testdata/cluster/"]), + (["tests/testdata/cluster2/"]), + (["tests/testdata/cluster3/"]), + (["tests/testdata/cluster6/"]), + (["--enable-helm", "--skip-crds", "tests/testdata/cluster/"]), + ( + [ + "--enable-helm", + "--skip-crds", + "tests/testdata/cluster6/", + "-a", + "batch/v1/CronJob", + ] + ), + (["--enable-helm", "--no-skip-secrets", "tests/testdata/cluster8/"]), + ], + ids=[ + "build", + "build-cluster2", + "build-cluster3", + "build-cluster6", + "build-helm-cluster", + "build-helm-cluster6", + "build-helm-cluster8-valuesFrom" + ], +) +async def test_build(args: list[str], snapshot: SnapshotAssertion) -> None: + """Test build commands.""" + result = await run_command(["build"] + args) + assert result == snapshot diff --git a/tests/tool/test_diagnostics.py b/tests/tool/test_diagnostics.py new file mode 100644 index 00000000..7d4792dc --- /dev/null +++ b/tests/tool/test_diagnostics.py @@ -0,0 +1,36 @@ +"""Tests for the flux-local `diagnostics` command.""" + +import pathlib +import tempfile + +from syrupy.assertion import SnapshotAssertion +import yaml +import pytest + +from . import run_command + + +async def test_diagnostics(snapshot: SnapshotAssertion) -> None: + """Test test get ks commands.""" + result = await run_command(["diagnostics"]) + assert result == snapshot + + +@pytest.mark.parametrize( + ("contents"), + [ + (yaml.dump_all([["entry"]]).encode()), + (yaml.dump_all(["entry", "var"]).encode()), + (b"\x00\x00\x00"), + ], + ids=["list", "scalar", "not-yaml"], +) +async def test_invalid_mapping(contents: bytes, snapshot: SnapshotAssertion) -> None: + """Test test get ks commands.""" + with tempfile.TemporaryDirectory() as tmp_dir: + example_file = pathlib.Path(tmp_dir) / "example.yaml" + example_file.write_bytes(contents) + + result = await run_command(["diagnostics", "--path", tmp_dir]) + result = result.replace(str(example_file), "<>") + assert result == snapshot diff --git a/tests/tool/test_diff.py b/tests/tool/test_diff.py index 22c574b7..77c7ea8e 100644 --- a/tests/tool/test_diff.py +++ b/tests/tool/test_diff.py @@ -9,9 +9,11 @@ import pytest import os +from syrupy.assertion import SnapshotAssertion + from flux_local import git_repo -from flux_local.command import Command, run +from . import run_command CLUSTER_DIR = "tests/testdata/cluster" @@ -21,7 +23,7 @@ os.environ.get("SKIP_DIFF_TESTS", False), reason="SKIP_DIFF_TESTS set in environment", ) -async def test_diff_ks() -> None: +async def test_diff_ks(snapshot: SnapshotAssertion) -> None: """Test a diff in resources within a Kustomization.""" repo = git_repo.git_repo() @@ -35,36 +37,14 @@ async def test_diff_ks() -> None: configmap = path / "apps/prod/configmap.yaml" configmap.write_text("") - result = await run( - Command( - [ - "flux-local", - "diff", - "ks", - "--path", - str(path), - "--path-orig", - str(path_orig), - ] - ) + result = await run_command( + [ + "diff", + "ks", + "--path", + str(path), + "--path-orig", + str(path_orig), + ] ) - - assert ( - result - == """--- tests/testdata/cluster/apps/prod Kustomization: flux-system/apps ConfigMap: podinfo/podinfo-config - -+++ tests/testdata/cluster/apps/prod Kustomization: flux-system/apps ConfigMap: podinfo/podinfo-config - -@@ -1,9 +0,0 @@ - ----- --apiVersion: v1 --data: -- foo: bar --kind: ConfigMap --metadata: -- name: podinfo-config -- namespace: podinfo -- -""" # noqa: E501 - ) + assert result == snapshot diff --git a/tests/tool/test_diff_hr.py b/tests/tool/test_diff_hr.py new file mode 100644 index 00000000..4b8fff27 --- /dev/null +++ b/tests/tool/test_diff_hr.py @@ -0,0 +1,91 @@ +"""Tests for the flux-local `diff hr` command.""" + +import pytest + +from syrupy.assertion import SnapshotAssertion + +from . import run_command + + +@pytest.mark.parametrize( + ("args", "env"), + [ + (["podinfo", "-n", "podinfo", "--path", "tests/testdata/cluster/"], {}), + ( + [ + "renovate", + "-n", + "default", + "--path", + "tests/testdata/cluster6/", + "-a", + "batch/v1/CronJob", + ], + {}, + ), + ( + [ + "podinfo", + "-n", + "podinfo", + "--path", + "tests/testdata/cluster/", + "-o", + "yaml", + ], + {}, + ), + (["podinfo", "--all-namespaces", "--path", "tests/testdata/cluster/"], {}), + (["--all-namespaces", "--path", "tests/testdata/cluster/"], {}), + ( + ["podinfo", "-n", "podinfo", "--path", "tests/testdata/cluster/"], + {"DIFF": "diff"}, + ), + ( + [ + "podinfo", + "-n", + "podinfo", + "--path", + "tests/testdata/cluster/", + "--limit-bytes", + "20", + ], + {"DIFF": "diff"}, + ), + ( + [ + "podinfo", + "-n", + "podinfo", + "--path", + "tests/testdata/cluster/", + "--limit-bytes", + "20000", + ], + {}, + ), + (["example", "--path", "tests/testdata/cluster"], {}), + (["example", "--namespace", "example", "--path", "tests/testdata/cluster"], {}), + (["example", "--all-namespaces", "--path", "tests/testdata/cluster"], {}), + ], + ids=[ + "podinfo", + "cluster6", + "yaml", + "all-namespaces", + "all", + "external", + "external-limit-bytes", + "limit-bytes", + "not-found", + "not-found-namespace", + "not-found-all-namespaces", + ], +) +async def test_diff_hr( + args: list[str], env: dict[str, str], snapshot: SnapshotAssertion +) -> None: + """Test test diff hr commands.""" + result = await run_command(["diff", "hr"] + args, env=env) + assert result == snapshot diff --git a/tests/tool/test_diff_ks.py b/tests/tool/test_diff_ks.py new file mode 100644 index 00000000..c6562619 --- /dev/null +++ b/tests/tool/test_diff_ks.py @@ -0,0 +1,48 @@ +"""Tests for the flux-local `diff ks` command.""" + +import pytest + +from syrupy.assertion import SnapshotAssertion + +from . import run_command + + +@pytest.mark.parametrize( + ("args", "env"), + [ + (["apps", "--path", "tests/testdata/cluster"], {}), + (["apps", "--path", "tests/testdata/cluster"], {"DIFF": "diff"}), + (["apps", "--path", "tests/testdata/cluster", "-o", "yaml"], {}), + ( + [ + "apps", + "--path", + "tests/testdata/cluster", + "-o", + "yaml", + "--limit-bytes", + "20000", + ], + {}, + ), + ( + [ + "apps", + "--path", + "tests/testdata/cluster", + "-o", + "yaml", + "--sources", + "''", + ], + {}, + ), + ], + ids=["apps", "ks-external", "yaml", "yaml-limit", "yaml-empty-sources"], +) +async def test_diff_ks( + args: list[str], env: dict[str, str], snapshot: SnapshotAssertion +) -> None: + """Test test diff ks commands.""" + result = await run_command(["diff", "ks"] + args, env=env) + assert result == snapshot diff --git a/tests/tool/test_flux_local.py b/tests/tool/test_flux_local.py deleted file mode 100644 index ea0a01bb..00000000 --- a/tests/tool/test_flux_local.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Tests for the flux-local command line tool.""" - -import pytest - -from pytest_golden.plugin import GoldenTestFixture - -from flux_local.command import Command, run - - -@pytest.mark.golden_test("testdata/*.yaml") -async def test_flux_local_golden(golden: GoldenTestFixture) -> None: - """Test commands in golden files.""" - env = golden.get("env") - args = golden["args"] - result = await run(Command(["flux-local"] + args, env=env)) - if golden.get("stdout"): - assert result == golden.out["stdout"] diff --git a/tests/tool/test_get_cluster.py b/tests/tool/test_get_cluster.py new file mode 100644 index 00000000..82eb8fbf --- /dev/null +++ b/tests/tool/test_get_cluster.py @@ -0,0 +1,55 @@ +"""Tests for the flux-local `get cluster` command.""" + +import pytest + +from syrupy.assertion import SnapshotAssertion + +from . import run_command + + +@pytest.mark.parametrize( + ("args"), + [ + (["--path", "tests/testdata/cluster"]), + (["--path", "tests/testdata/cluster2"]), + (["--path", "tests/testdata/cluster3"]), + ( + [ + "--path", + "tests/testdata/cluster3", + "--sources", + "cluster=tests/testdata/cluster3", + ] + ), + (["--path", "tests/testdata/cluster4"]), + (["--path", "tests/testdata/cluster5"]), + (["--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", "--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"]), + ], + ids=[ + "cluster", + "cluster2", + "cluster3-no-source", + "cluster3", + "cluster4", + "cluster5", + "cluster6", + "cluster7", + "all-namespaces", + "yaml-cluster-no-images", + "yaml-cluster-images", + "yaml-cluster8-no-images", + "yaml-cluster8-images", + "yaml-cluster8-images-allow-secrets", + ], +) +async def test_get_cluster(args: list[str], snapshot: SnapshotAssertion) -> None: + """Test test get cluster commands.""" + result = await run_command(["get", "cluster"] + args) + assert result == snapshot diff --git a/tests/tool/test_get_hr.py b/tests/tool/test_get_hr.py new file mode 100644 index 00000000..e53a0923 --- /dev/null +++ b/tests/tool/test_get_hr.py @@ -0,0 +1,48 @@ +"""Tests for the flux-local `get hr` command.""" + +import pytest + +from syrupy.assertion import SnapshotAssertion + +from . import run_command + + +@pytest.mark.parametrize( + ("args"), + [ + (["-A", "--path", "tests/testdata/cluster"]), + (["-A", "--path", "tests/testdata/cluster2"]), + (["-A", "--path", "tests/testdata/cluster3"]), + ( + [ + "-A", + "--path", + "tests/testdata/cluster3", + "--sources", + "cluster=tests/testdata/cluster3", + ] + ), + (["-A", "--path", "tests/testdata/cluster4"]), + (["-A", "--path", "tests/testdata/cluster5"]), + (["-A", "--path", "tests/testdata/cluster6"]), + (["-A", "--path", "tests/testdata/cluster7"]), + (["metallb", "-A", "--path", "tests/testdata/cluster"]), + (["-n", "metallb", "--path", "tests/testdata/cluster"]), + ], + ids=[ + "cluster", + "cluster2", + "cluster3-no-source", + "cluster3", + "cluster4", + "cluster5", + "cluster6", + "cluster7", + "all_namespace", + "name", + ], +) +async def test_get_hr(args: list[str], snapshot: SnapshotAssertion) -> None: + """Test test get hr commands.""" + result = await run_command(["get", "hr"] + args) + assert result == snapshot diff --git a/tests/tool/test_get_ks.py b/tests/tool/test_get_ks.py new file mode 100644 index 00000000..4ee33d6a --- /dev/null +++ b/tests/tool/test_get_ks.py @@ -0,0 +1,50 @@ +"""Tests for the flux-local `get ks` command.""" + +import pytest + +from syrupy.assertion import SnapshotAssertion + +from . import run_command + + +@pytest.mark.parametrize( + ("args"), + [ + (["--path", "tests/testdata/cluster"]), + (["--path", "tests/testdata/cluster2"]), + ( + [ + "-A", + "--path", + "tests/testdata/cluster3", + "--sources", + "cluster=tests/testdata/cluster3", + ] + ), + (["--path", "tests/testdata/cluster4"]), + (["--path", "tests/testdata/cluster5"]), + (["--all", "--path", "tests/testdata/cluster5"]), + (["--path", "tests/testdata/cluster6"]), + (["--path", "tests/testdata/cluster7"]), + (["--path", "./tests/testdata/cluster/clusters/prod"]), + (["--path", "tests/testdata/cluster", "-o", "wide"]), + (["--all-namespaces", "--path", "./tests/testdata/cluster/apps/prod"]), + ], + ids=[ + "cluster", + "cluster2", + "cluster3", + "cluster4", + "cluster5", + "cluster5-all", + "cluster6", + "cluster7", + "cluster_path", + "wide", + "ks_path", + ], +) +async def test_get_ks(args: list[str], snapshot: SnapshotAssertion) -> None: + """Test test get ks commands.""" + result = await run_command(["get", "ks"] + args) + assert result == snapshot diff --git a/tests/tool/test_test.py b/tests/tool/test_test.py new file mode 100644 index 00000000..e8cd2372 --- /dev/null +++ b/tests/tool/test_test.py @@ -0,0 +1,35 @@ +"""Tests for the flux-local `test` command.""" + +import pytest + +from . import run_command + + +@pytest.mark.parametrize( + ("args"), + [ + (["tests/testdata/cluster"]), + ], + ids=["cluster"], +) +async def test_test_ks(args: list[str]) -> None: + """Test test kustomization commands.""" + result = await run_command(["test"] + args) + assert "passed" in result + + +@pytest.mark.parametrize( + ("args"), + [ + (["tests/testdata/cluster"]), + (["tests/testdata/cluster2"]), + (["--sources", "cluster=tests/testdata/cluster3", "tests/testdata/cluster3"]), + (["--enable-kyverno", "tests/testdata/cluster"]), + (["--enable-kyverno", "tests/testdata/cluster2"]), + ], + ids=["cluster", "cluster2", "cluster3", "policy", "policy-cluster2"], +) +async def test_test_hr(args: list[str]) -> None: + """Test test helmrelease commands.""" + result = await run_command(["test", "--enable-helm"] + args) + assert "passed" in result diff --git a/tests/tool/testdata/build.yaml b/tests/tool/testdata/build.yaml deleted file mode 100644 index d94f4a23..00000000 --- a/tests/tool/testdata/build.yaml +++ /dev/null @@ -1,269 +0,0 @@ -args: -- build -- tests/testdata/cluster/ -stdout: |+ - --- - apiVersion: v1 - kind: Namespace - metadata: - name: podinfo - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - --- - apiVersion: v1 - data: - foo: bar - kind: ConfigMap - metadata: - name: podinfo-config - namespace: podinfo - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: podinfo - namespace: podinfo - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - spec: - chart: - spec: - chart: podinfo - sourceRef: - kind: HelmRepository - name: podinfo - namespace: flux-system - version: 6.3.2 - install: - remediation: - retries: 3 - interval: 50m - releaseName: podinfo - values: - ingress: - className: nginx - enabled: true - hosts: - - host: podinfo.production - paths: - - path: / - pathType: ImplementationSpecific - redis: - enabled: true - repository: public.ecr.aws/docker/library/redis - tag: 7.0.6 - - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: apps - namespace: flux-system - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - interval: 10m0s - dependsOn: - - name: infra-configs - sourceRef: - kind: GitRepository - name: flux-system - path: ./tests/testdata/cluster/apps/prod - prune: true - wait: true - timeout: 5m0s - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: flux-system - namespace: flux-system - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - interval: 10m0s - path: ./tests/testdata/cluster/clusters/prod - prune: true - sourceRef: - kind: GitRepository - name: flux-system - --- - apiVersion: source.toolkit.fluxcd.io/v1 - kind: GitRepository - metadata: - name: flux-system - namespace: flux-system - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - 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: infra-controllers - namespace: flux-system - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - spec: - interval: 1h - retryInterval: 1m - timeout: 5m - sourceRef: - kind: GitRepository - name: flux-system - path: ./tests/testdata/cluster/infrastructure/controllers - prune: true - wait: true - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: infra-configs - namespace: flux-system - annotations: - config.kubernetes.io/index: '4' - internal.config.kubernetes.io/index: '4' - spec: - dependsOn: - - name: infra-controllers - interval: 1h - retryInterval: 1m - timeout: 5m - sourceRef: - kind: GitRepository - name: flux-system - path: ./tests/testdata/cluster/infrastructure/configs - prune: true - - --- - apiVersion: kyverno.io/v1 - kind: ClusterPolicy - metadata: - annotations: - policies.kyverno.io/description: Policy that is expected to allow resources under test through since no resources should have this annotation. - policies.kyverno.io/title: Test Allow Policy - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - name: test-allow-policy - spec: - background: true - rules: - - match: - resources: - kinds: - - ConfigMap - name: forbid-test-annotation - validate: - message: Found test-annotation - pattern: - metadata: - =(annotations): - X(flux-local/test-annotation): "null" - validationFailureAction: audit - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: bitnami - namespace: flux-system - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - interval: 30m - provider: generic - timeout: 1m0s - url: https://charts.bitnami.com/bitnami - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: podinfo - namespace: flux-system - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - spec: - interval: 5m - type: oci - url: oci://ghcr.io/stefanprodan/charts - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: weave-charts - namespace: flux-system - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - spec: - interval: 120m - type: oci - url: oci://ghcr.io/weaveworks/charts - - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: weave-gitops - namespace: flux-system - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - chart: - spec: - chart: weave-gitops - interval: 12h - sourceRef: - kind: HelmRepository - name: weave-charts - version: 4.0.22 - interval: 60m - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: metallb - namespace: metallb - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - chart: - spec: - chart: metallb - reconcileStrategy: ChartVersion - sourceRef: - kind: HelmRepository - name: bitnami - namespace: flux-system - version: 4.1.14 - install: - crds: CreateReplace - remediation: - retries: 3 - interval: 5m - releaseName: metallb - upgrade: - crds: CreateReplace - values: - speaker: - secretName: metallb-secret - -... diff --git a/tests/tool/testdata/build6.yaml b/tests/tool/testdata/build6.yaml deleted file mode 100644 index df767338..00000000 --- a/tests/tool/testdata/build6.yaml +++ /dev/null @@ -1,107 +0,0 @@ -args: -- build -- tests/testdata/cluster6/ -stdout: |+ - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: renovate - namespace: default - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - chart: - spec: - chart: renovate - sourceRef: - kind: HelmRepository - name: renovate - namespace: flux-system - interval: 5m - values: - renovate: - existingConfigFile: /dev/null - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - resources: - limits: - cpu: 1000m - memory: 2G - requests: - cpu: 100m - memory: 256Mi - securityContext: - fsGroup: 1000 - fsGroupChangePolicy: Always - runAsNonRoot: true - runAsUser: 1000 - seccompProfile: - type: RuntimeDefault - serviceAccount: - create: true - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: renovate - namespace: flux-system - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - interval: 30m - url: https://docs.renovatebot.com/helm-charts - - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: apps - namespace: flux-system - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - path: ./tests/testdata/cluster6/apps/ - sourceRef: - kind: GitRepository - name: flux-system - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: flux-system - namespace: flux-system - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - interval: 10m0s - path: ./tests/testdata/cluster6/cluster - prune: true - sourceRef: - kind: GitRepository - name: flux-system - --- - apiVersion: source.toolkit.fluxcd.io/v1 - kind: GitRepository - metadata: - name: flux-system - namespace: flux-system - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - spec: - interval: 1m0s - ref: - branch: main - secretRef: - name: flux-system - url: ssh://git@github.com/allenporter/flux-local - -... diff --git a/tests/tool/testdata/build_helm.yaml b/tests/tool/testdata/build_helm.yaml deleted file mode 100644 index b88f954a..00000000 --- a/tests/tool/testdata/build_helm.yaml +++ /dev/null @@ -1,1638 +0,0 @@ -args: -- build -- --enable-helm -- --skip-crds -- tests/testdata/cluster/ -stdout: |+ - --- - apiVersion: v1 - kind: Namespace - metadata: - name: podinfo - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - --- - apiVersion: v1 - data: - foo: bar - kind: ConfigMap - metadata: - name: podinfo-config - namespace: podinfo - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: podinfo - namespace: podinfo - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - spec: - chart: - spec: - chart: podinfo - sourceRef: - kind: HelmRepository - name: podinfo - namespace: flux-system - version: 6.3.2 - install: - remediation: - retries: 3 - interval: 50m - releaseName: podinfo - values: - ingress: - className: nginx - enabled: true - hosts: - - host: podinfo.production - paths: - - path: / - pathType: ImplementationSpecific - redis: - enabled: true - repository: public.ecr.aws/docker/library/redis - tag: 7.0.6 - - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: apps - namespace: flux-system - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - interval: 10m0s - dependsOn: - - name: infra-configs - sourceRef: - kind: GitRepository - name: flux-system - path: ./tests/testdata/cluster/apps/prod - prune: true - wait: true - timeout: 5m0s - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: flux-system - namespace: flux-system - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - interval: 10m0s - path: ./tests/testdata/cluster/clusters/prod - prune: true - sourceRef: - kind: GitRepository - name: flux-system - --- - apiVersion: source.toolkit.fluxcd.io/v1 - kind: GitRepository - metadata: - name: flux-system - namespace: flux-system - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - 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: infra-controllers - namespace: flux-system - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - spec: - interval: 1h - retryInterval: 1m - timeout: 5m - sourceRef: - kind: GitRepository - name: flux-system - path: ./tests/testdata/cluster/infrastructure/controllers - prune: true - wait: true - --- - apiVersion: kustomize.toolkit.fluxcd.io/v1 - kind: Kustomization - metadata: - name: infra-configs - namespace: flux-system - annotations: - config.kubernetes.io/index: '4' - internal.config.kubernetes.io/index: '4' - spec: - dependsOn: - - name: infra-controllers - interval: 1h - retryInterval: 1m - timeout: 5m - sourceRef: - kind: GitRepository - name: flux-system - path: ./tests/testdata/cluster/infrastructure/configs - prune: true - - --- - apiVersion: kyverno.io/v1 - kind: ClusterPolicy - metadata: - annotations: - policies.kyverno.io/description: Policy that is expected to allow resources under test through since no resources should have this annotation. - policies.kyverno.io/title: Test Allow Policy - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - name: test-allow-policy - spec: - background: true - rules: - - match: - resources: - kinds: - - ConfigMap - name: forbid-test-annotation - validate: - message: Found test-annotation - pattern: - metadata: - =(annotations): - X(flux-local/test-annotation): "null" - validationFailureAction: audit - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: bitnami - namespace: flux-system - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - interval: 30m - provider: generic - timeout: 1m0s - url: https://charts.bitnami.com/bitnami - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: podinfo - namespace: flux-system - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - spec: - interval: 5m - type: oci - url: oci://ghcr.io/stefanprodan/charts - --- - apiVersion: source.toolkit.fluxcd.io/v1beta2 - kind: HelmRepository - metadata: - name: weave-charts - namespace: flux-system - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - spec: - interval: 120m - type: oci - url: oci://ghcr.io/weaveworks/charts - - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: weave-gitops - namespace: flux-system - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - chart: - spec: - chart: weave-gitops - interval: 12h - sourceRef: - kind: HelmRepository - name: weave-charts - version: 4.0.22 - interval: 60m - --- - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: metallb - namespace: metallb - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - chart: - spec: - chart: metallb - reconcileStrategy: ChartVersion - sourceRef: - kind: HelmRepository - name: bitnami - namespace: flux-system - version: 4.1.14 - install: - crds: CreateReplace - remediation: - retries: 3 - interval: 5m - releaseName: metallb - upgrade: - crds: CreateReplace - values: - speaker: - secretName: metallb-secret - - --- - # Source: weave-gitops/templates/network-policy.yaml - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: allow-dashboard-ingress - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - spec: - podSelector: - matchLabels: - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - ingress: - - ports: - - port: 9001 - protocol: TCP - policyTypes: - - Ingress - --- - # Source: weave-gitops/templates/network-policy.yaml - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: allow-dashboard-egress - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - podSelector: - matchLabels: - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - egress: - - {} - policyTypes: - - Egress - --- - # Source: weave-gitops/templates/serviceaccount.yaml - apiVersion: v1 - kind: ServiceAccount - metadata: - name: weave-gitops - labels: - helm.sh/chart: weave-gitops-4.0.22 - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - app.kubernetes.io/version: "v0.24.0" - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - --- - # Source: weave-gitops/templates/role.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: - name: weave-gitops - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - rules: - # impersonation rules for ui calls - - apiGroups: [""] - resources: ["users", "groups"] - verbs: ["impersonate"] - # Access to enterprise entitlement - - apiGroups: [""] - resources: ["secrets"] - verbs: ["get", "list"] - # or should return the first non-falsy result - resourceNames: ["cluster-user-auth", "oidc-auth"] - # The service account needs to read namespaces to know where it can query - - apiGroups: [""] - resources: ["namespaces"] - verbs: ["get", "list", "watch"] - # The service account needs to list custom resources to query if given feature - # is available or not. - - apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["list"] - --- - # Source: weave-gitops/templates/rolebinding.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: - name: weave-gitops - labels: - helm.sh/chart: weave-gitops-4.0.22 - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - app.kubernetes.io/version: "v0.24.0" - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '4' - internal.config.kubernetes.io/index: '4' - subjects: - - kind: ServiceAccount - name: weave-gitops - namespace: flux-system - roleRef: - kind: ClusterRole - name: weave-gitops - apiGroup: rbac.authorization.k8s.io - --- - # Source: weave-gitops/templates/service.yaml - apiVersion: v1 - kind: Service - metadata: - name: weave-gitops - labels: - helm.sh/chart: weave-gitops-4.0.22 - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - app.kubernetes.io/version: "v0.24.0" - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '5' - internal.config.kubernetes.io/index: '5' - spec: - type: ClusterIP - ports: - - port: 9001 - targetPort: http - protocol: TCP - name: http - selector: - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - --- - # Source: weave-gitops/templates/deployment.yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: weave-gitops - labels: - helm.sh/chart: weave-gitops-4.0.22 - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - app.kubernetes.io/version: "v0.24.0" - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/part-of: weave-gitops - weave.works/app: weave-gitops-oss - annotations: - config.kubernetes.io/index: '6' - internal.config.kubernetes.io/index: '6' - spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - template: - metadata: - labels: - app.kubernetes.io/name: weave-gitops - app.kubernetes.io/instance: weave-gitops - app.kubernetes.io/part-of: weave-gitops - weave.works/app: weave-gitops-oss - spec: - serviceAccountName: weave-gitops - securityContext: {} - containers: - - name: weave-gitops - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1000 - seccompProfile: - type: RuntimeDefault - image: "ghcr.io/weaveworks/wego-app:v0.24.0" - imagePullPolicy: IfNotPresent - args: - - "--log-level" - - "info" - - "--insecure" - ports: - - name: http - containerPort: 9001 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http - env: - - name: WEAVE_GITOPS_FEATURE_TENANCY - value: "true" - - name: WEAVE_GITOPS_FEATURE_CLUSTER - value: "false" - resources: {} - - --- - # Source: metallb/templates/controller/serviceaccount.yaml - apiVersion: v1 - kind: ServiceAccount - metadata: - name: metallb-controller - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - automountServiceAccountToken: true - --- - # Source: metallb/templates/speaker/serviceaccount.yaml - apiVersion: v1 - kind: ServiceAccount - metadata: - name: metallb-speaker - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - automountServiceAccountToken: true - --- - # Source: metallb/templates/controller/configmap.yaml - apiVersion: v1 - kind: ConfigMap - metadata: - name: metallb-config - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - data: - config: | - null - --- - # Source: metallb/templates/controller/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: - name: metallb:controller - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - annotations: - config.kubernetes.io/index: '11' - internal.config.kubernetes.io/index: '11' - rules: - - apiGroups: - - '' - resources: - - services - verbs: - - get - - list - - watch - - update - - apiGroups: - - '' - resources: - - services/status - verbs: - - update - - apiGroups: - - '' - resources: - - events - verbs: - - create - - patch - - apiGroups: - - policy - resourceNames: - - metallb-controller - resources: - - podsecuritypolicies - verbs: - - use - - apiGroups: - - admissionregistration.k8s.io - resources: - - validatingwebhookconfigurations - - mutatingwebhookconfigurations - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - apiextensions.k8s.io - resources: - - customresourcedefinitions - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - --- - # Source: metallb/templates/speaker/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: - name: metallb-speaker - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - annotations: - config.kubernetes.io/index: '12' - internal.config.kubernetes.io/index: '12' - rules: - - apiGroups: - - '' - resources: - - services - - endpoints - - nodes - verbs: - - get - - list - - watch - - apiGroups: - - '' - resources: - - events - verbs: - - create - - patch - - apiGroups: - - policy - resourceNames: - - metallb-speaker - resources: - - podsecuritypolicies - verbs: - - use - - apiGroups: - - discovery.k8s.io - resources: - - endpointslices - verbs: - - get - - list - - watch - --- - # Source: metallb/templates/controller/rbac.yaml - ## Role bindings - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: - name: metallb:controller - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - annotations: - config.kubernetes.io/index: '13' - internal.config.kubernetes.io/index: '13' - subjects: - - kind: ServiceAccount - name: metallb-controller - namespace: metallb - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: metallb:controller - --- - # Source: metallb/templates/speaker/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: - name: metallb-speaker - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - annotations: - config.kubernetes.io/index: '14' - internal.config.kubernetes.io/index: '14' - subjects: - - kind: ServiceAccount - name: metallb-speaker - namespace: "metallb" - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: metallb-speaker - --- - # Source: metallb/templates/controller/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - name: metallb-controller - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - annotations: - config.kubernetes.io/index: '15' - internal.config.kubernetes.io/index: '15' - rules: - - apiGroups: - - '' - resources: - - secrets - verbs: - - create - - get - - list - - watch - - apiGroups: - - '' - resources: - - secrets - resourceNames: - - "metallb-secret" - verbs: - - list - - apiGroups: - - apps - resources: - - deployments - resourceNames: - - metallb-controller - verbs: - - get - - apiGroups: - - '' - resources: - - secrets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - metallb.io - resources: - - addresspools - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - ipaddresspools - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - bgppeers - verbs: - - get - - list - - apiGroups: - - metallb.io - resources: - - bgpadvertisements - verbs: - - get - - list - - apiGroups: - - metallb.io - resources: - - l2advertisements - verbs: - - get - - list - - apiGroups: - - metallb.io - resources: - - communities - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - bfdprofiles - verbs: - - get - - list - - watch - --- - # Source: metallb/templates/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - name: metallb-config-watcher - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '16' - internal.config.kubernetes.io/index: '16' - rules: - - apiGroups: - - '' - resources: - - configmaps - verbs: - - get - - list - - watch - --- - # Source: metallb/templates/speaker/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - name: metallb-pod-lister - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - annotations: - config.kubernetes.io/index: '17' - internal.config.kubernetes.io/index: '17' - rules: - - apiGroups: - - '' - resources: - - pods - verbs: - - list - - apiGroups: - - '' - resources: - - secrets - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - addresspools - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - bfdprofiles - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - bgppeers - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - l2advertisements - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - bgpadvertisements - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - ipaddresspools - verbs: - - get - - list - - watch - - apiGroups: - - metallb.io - resources: - - communities - verbs: - - get - - list - - watch - --- - # Source: metallb/templates/controller/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - name: metallb-controller - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - annotations: - config.kubernetes.io/index: '18' - internal.config.kubernetes.io/index: '18' - subjects: - - kind: ServiceAccount - name: metallb-controller - namespace: metallb - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: metallb-controller - --- - # Source: metallb/templates/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - name: metallb-config-watcher - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '19' - internal.config.kubernetes.io/index: '19' - subjects: - - kind: ServiceAccount - name: metallb-controller - - kind: ServiceAccount - name: metallb-speaker - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: metallb-config-watcher - --- - # Source: metallb/templates/speaker/rbac.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - name: metallb-pod-lister - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - annotations: - config.kubernetes.io/index: '20' - internal.config.kubernetes.io/index: '20' - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: metallb-pod-lister - subjects: - - kind: ServiceAccount - name: metallb-speaker - --- - # Source: metallb/templates/controller/webhooks.yaml - apiVersion: v1 - kind: Service - metadata: - name: metallb-webhook-service - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '21' - internal.config.kubernetes.io/index: '21' - spec: - ports: - - port: 443 - targetPort: 9443 - selector: - app.kubernetes.io/name: metallb - app.kubernetes.io/instance: metallb - app.kubernetes.io/component: controller - --- - # Source: metallb/templates/speaker/daemonset.yaml - apiVersion: apps/v1 - kind: DaemonSet - metadata: - name: metallb-speaker - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - annotations: - config.kubernetes.io/index: '22' - internal.config.kubernetes.io/index: '22' - spec: - updateStrategy: - type: RollingUpdate - selector: - matchLabels: - app.kubernetes.io/name: metallb - app.kubernetes.io/instance: metallb - app.kubernetes.io/component: speaker - template: - metadata: - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: speaker - spec: - serviceAccountName: metallb-speaker - affinity: - podAffinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/name: metallb - app.kubernetes.io/instance: metallb - app.kubernetes.io/component: speaker - topologyKey: kubernetes.io/hostname - weight: 1 - nodeAffinity: - nodeSelector: - "kubernetes.io/os": linux - hostNetwork: true - securityContext: - fsGroup: 0 - terminationGracePeriodSeconds: 2 - containers: - - name: metallb-speaker - image: docker.io/bitnami/metallb-speaker:0.13.7-debian-11-r28 - imagePullPolicy: IfNotPresent - securityContext: - allowPrivilegeEscalation: false - capabilities: - add: - - NET_ADMIN - - NET_RAW - - SYS_ADMIN - drop: - - ALL - readOnlyRootFilesystem: true - runAsUser: 0 - args: - - --port=7472 - env: - - name: METALLB_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: METALLB_HOST - valueFrom: - fieldRef: - fieldPath: status.hostIP - - name: METALLB_ML_BIND_ADDR - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: METALLB_ML_LABELS - value: "app.kubernetes.io/name=metallb,app.kubernetes.io/instance=metallb,app.kubernetes.io/component=speaker" - - name: METALLB_ML_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: METALLB_ML_SECRET_KEY - valueFrom: - secretKeyRef: - name: metallb-secret - key: secretkey - envFrom: - ports: - - name: metrics - containerPort: 7472 - livenessProbe: - failureThreshold: 3 - initialDelaySeconds: 10 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - httpGet: - path: /metrics - port: metrics - readinessProbe: - failureThreshold: 3 - initialDelaySeconds: 10 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - httpGet: - path: /metrics - port: metrics - resources: - limits: {} - requests: {} - --- - # Source: metallb/templates/controller/deployment.yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: metallb-controller - namespace: "metallb" - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - annotations: - config.kubernetes.io/index: '23' - internal.config.kubernetes.io/index: '23' - spec: - replicas: 1 - strategy: - type: RollingUpdate - revisionHistoryLimit: 3 - selector: - matchLabels: - app.kubernetes.io/name: metallb - app.kubernetes.io/instance: metallb - app.kubernetes.io/component: controller - template: - metadata: - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/component: controller - spec: - serviceAccountName: metallb-controller - nodeSelector: - "kubernetes.io/os": linux - affinity: - podAffinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/name: metallb - app.kubernetes.io/instance: metallb - app.kubernetes.io/component: controller - topologyKey: kubernetes.io/hostname - weight: 1 - nodeAffinity: - securityContext: - fsGroup: 1001 - containers: - - name: metallb-controller - image: docker.io/bitnami/metallb-controller:0.13.7-debian-11-r29 - imagePullPolicy: IfNotPresent - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1001 - args: - - --port=7472 - - --cert-service-name=metallb-webhook-service - env: - envFrom: - ports: - - name: metrics - containerPort: 7472 - - name: webhook-server - containerPort: 9443 - protocol: TCP - volumeMounts: - - name: cert - mountPath: /tmp/k8s-webhook-server/serving-certs - readOnly: true - livenessProbe: - failureThreshold: 3 - initialDelaySeconds: 10 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - httpGet: - path: /metrics - port: metrics - readinessProbe: - failureThreshold: 3 - initialDelaySeconds: 10 - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - httpGet: - path: /metrics - port: metrics - resources: - limits: {} - requests: {} - volumes: - - name: cert - secret: - defaultMode: 420 - secretName: webhook-server-cert - --- - # Source: metallb/templates/controller/webhooks.yaml - apiVersion: admissionregistration.k8s.io/v1 - kind: ValidatingWebhookConfiguration - metadata: - name: metallb-webhook-configuration - labels: - app.kubernetes.io/name: metallb - helm.sh/chart: metallb-4.1.14 - app.kubernetes.io/instance: metallb - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '24' - internal.config.kubernetes.io/index: '24' - webhooks: - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta1-addresspool - failurePolicy: Fail - name: addresspoolvalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta1 - operations: - - CREATE - - UPDATE - resources: - - addresspools - sideEffects: None - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta2-bgppeer - failurePolicy: Fail - name: bgppeervalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta2 - operations: - - CREATE - - UPDATE - resources: - - bgppeers - sideEffects: None - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta1-ipaddresspool - failurePolicy: Fail - name: ipaddresspoolvalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta1 - operations: - - CREATE - - UPDATE - resources: - - ipaddresspools - sideEffects: None - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta1-bgpadvertisement - failurePolicy: Fail - name: bgpadvertisementvalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta1 - operations: - - CREATE - - UPDATE - resources: - - bgpadvertisements - sideEffects: None - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta1-community - failurePolicy: Fail - name: communityvalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta1 - operations: - - CREATE - - UPDATE - resources: - - communities - sideEffects: None - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta1-bfdprofile - failurePolicy: Fail - name: bfdprofileyvalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta1 - operations: - - DELETE - resources: - - bfdprofiles - sideEffects: None - - admissionReviewVersions: - - v1 - clientConfig: - service: - name: metallb-webhook-service - namespace: "metallb" - path: /validate-metallb-io-v1beta1-l2advertisement - failurePolicy: Fail - name: l2advertisementvalidationwebhook.metallb.io - rules: - - apiGroups: - - metallb.io - apiVersions: - - v1beta1 - operations: - - CREATE - - UPDATE - resources: - - l2advertisements - sideEffects: None - - --- - # Source: podinfo/templates/redis/config.yaml - apiVersion: v1 - kind: ConfigMap - metadata: - name: podinfo-redis - annotations: - config.kubernetes.io/index: '0' - internal.config.kubernetes.io/index: '0' - data: - redis.conf: | - maxmemory 64mb - maxmemory-policy allkeys-lru - save "" - appendonly no - --- - # Source: podinfo/templates/redis/service.yaml - apiVersion: v1 - kind: Service - metadata: - name: podinfo-redis - labels: - app: podinfo-redis - annotations: - config.kubernetes.io/index: '1' - internal.config.kubernetes.io/index: '1' - spec: - type: ClusterIP - selector: - app: podinfo-redis - ports: - - name: redis - port: 6379 - protocol: TCP - targetPort: redis - --- - # Source: podinfo/templates/service.yaml - apiVersion: v1 - kind: Service - metadata: - name: podinfo - labels: - helm.sh/chart: podinfo-6.3.2 - app.kubernetes.io/name: podinfo - app.kubernetes.io/version: "6.3.2" - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '2' - internal.config.kubernetes.io/index: '2' - spec: - type: ClusterIP - ports: - - port: 9898 - targetPort: http - protocol: TCP - name: http - - port: 9999 - targetPort: grpc - protocol: TCP - name: grpc - selector: - app.kubernetes.io/name: podinfo - --- - # Source: podinfo/templates/deployment.yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: podinfo - labels: - helm.sh/chart: podinfo-6.3.2 - app.kubernetes.io/name: podinfo - app.kubernetes.io/version: "6.3.2" - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '3' - internal.config.kubernetes.io/index: '3' - spec: - replicas: 1 - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 1 - selector: - matchLabels: - app.kubernetes.io/name: podinfo - template: - metadata: - labels: - app.kubernetes.io/name: podinfo - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9898" - spec: - terminationGracePeriodSeconds: 30 - containers: - - name: podinfo - image: "ghcr.io/stefanprodan/podinfo:6.3.2" - imagePullPolicy: IfNotPresent - command: - - ./podinfo - - --port=9898 - - --cert-path=/data/cert - - --port-metrics=9797 - - --grpc-port=9999 - - --grpc-service-name=podinfo - - --cache-server=tcp://podinfo-redis:6379 - - --level=info - - --random-delay=false - - --random-error=false - env: - - name: PODINFO_UI_COLOR - value: "#34577c" - ports: - - name: http - containerPort: 9898 - protocol: TCP - - name: http-metrics - containerPort: 9797 - protocol: TCP - - name: grpc - containerPort: 9999 - protocol: TCP - livenessProbe: - exec: - command: - - podcli - - check - - http - - localhost:9898/healthz - initialDelaySeconds: 1 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - periodSeconds: 10 - readinessProbe: - exec: - command: - - podcli - - check - - http - - localhost:9898/readyz - initialDelaySeconds: 1 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 - periodSeconds: 10 - volumeMounts: - - name: data - mountPath: /data - resources: - limits: null - requests: - cpu: 1m - memory: 16Mi - volumes: - - name: data - emptyDir: {} - --- - # Source: podinfo/templates/redis/deployment.yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: podinfo-redis - labels: - app: podinfo-redis - annotations: - config.kubernetes.io/index: '4' - internal.config.kubernetes.io/index: '4' - spec: - strategy: - type: Recreate - selector: - matchLabels: - app: podinfo-redis - template: - metadata: - labels: - app: podinfo-redis - annotations: - checksum/config: "34c601c9d39797bbf53d1c7a278976609301f637ec11dc0253563729dfad4f8e" - spec: - containers: - - name: redis - image: "public.ecr.aws/docker/library/redis:7.0.6" - imagePullPolicy: IfNotPresent - command: - - redis-server - - "/redis-master/redis.conf" - ports: - - name: redis - containerPort: 6379 - protocol: TCP - livenessProbe: - tcpSocket: - port: redis - initialDelaySeconds: 5 - timeoutSeconds: 5 - readinessProbe: - exec: - command: - - redis-cli - - ping - initialDelaySeconds: 5 - timeoutSeconds: 5 - resources: - limits: - cpu: 1000m - memory: 128Mi - requests: - cpu: 100m - memory: 32Mi - volumeMounts: - - mountPath: /var/lib/redis - name: data - - mountPath: /redis-master - name: config - volumes: - - name: data - emptyDir: {} - - name: config - configMap: - name: podinfo-redis - items: - - key: redis.conf - path: redis.conf - --- - # Source: podinfo/templates/ingress.yaml - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: podinfo - labels: - helm.sh/chart: podinfo-6.3.2 - app.kubernetes.io/name: podinfo - app.kubernetes.io/version: "6.3.2" - app.kubernetes.io/managed-by: Helm - annotations: - config.kubernetes.io/index: '5' - internal.config.kubernetes.io/index: '5' - spec: - ingressClassName: nginx - rules: - - host: "podinfo.production" - http: - paths: - - path: / - pathType: ImplementationSpecific - backend: - service: - name: podinfo - port: - number: 9898 - -... diff --git a/tests/tool/testdata/build_helm6.yaml b/tests/tool/testdata/build_helm6.yaml deleted file mode 100644 index 99795f62..00000000 --- a/tests/tool/testdata/build_helm6.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- build -- --enable-helm -- --skip-crds -- tests/testdata/cluster6/ -- -a -- batch/v1/CronJob -stdout: |+ diff --git a/tests/tool/testdata/diff_hr.yaml b/tests/tool/testdata/diff_hr.yaml deleted file mode 100644 index d763eddb..00000000 --- a/tests/tool/testdata/diff_hr.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- diff -- hr -- podinfo -- -n -- podinfo -- --path -- tests/testdata/cluster/ -stdout: '' diff --git a/tests/tool/testdata/diff_hr6.yaml b/tests/tool/testdata/diff_hr6.yaml deleted file mode 100644 index dc79a1bf..00000000 --- a/tests/tool/testdata/diff_hr6.yaml +++ /dev/null @@ -1,11 +0,0 @@ -args: -- diff -- hr -- renovate -- -n -- default -- --path -- tests/testdata/cluster6/ -- -a -- batch/v1/CronJob -stdout: '' diff --git a/tests/tool/testdata/diff_hr_all.yaml b/tests/tool/testdata/diff_hr_all.yaml deleted file mode 100644 index 83e1426c..00000000 --- a/tests/tool/testdata/diff_hr_all.yaml +++ /dev/null @@ -1,7 +0,0 @@ -args: -- diff -- hr -- --all-namespaces -- --path -- tests/testdata/cluster -stdout: '' diff --git a/tests/tool/testdata/diff_hr_all_namespaces.yaml b/tests/tool/testdata/diff_hr_all_namespaces.yaml deleted file mode 100644 index afcdb074..00000000 --- a/tests/tool/testdata/diff_hr_all_namespaces.yaml +++ /dev/null @@ -1,7 +0,0 @@ -args: -- diff -- hr -- podinfo -- --all-namespaces -- --path -- tests/testdata/cluster diff --git a/tests/tool/testdata/diff_hr_external.yaml b/tests/tool/testdata/diff_hr_external.yaml deleted file mode 100644 index f1a2bbc1..00000000 --- a/tests/tool/testdata/diff_hr_external.yaml +++ /dev/null @@ -1,11 +0,0 @@ -env: - DIFF: diff -args: -- diff -- hr -- podinfo -- -n -- podinfo -- --path -- tests/testdata/cluster/ -stdout: '' diff --git a/tests/tool/testdata/diff_hr_external_limit.yaml b/tests/tool/testdata/diff_hr_external_limit.yaml deleted file mode 100644 index 928eefb9..00000000 --- a/tests/tool/testdata/diff_hr_external_limit.yaml +++ /dev/null @@ -1,13 +0,0 @@ -env: - DIFF: diff -args: -- diff -- hr -- podinfo -- -n -- podinfo -- --path -- tests/testdata/cluster/ -- --limit-bytes -- '20' -stdout: '' diff --git a/tests/tool/testdata/diff_hr_limit.yaml b/tests/tool/testdata/diff_hr_limit.yaml deleted file mode 100644 index 5e3b9eeb..00000000 --- a/tests/tool/testdata/diff_hr_limit.yaml +++ /dev/null @@ -1,11 +0,0 @@ -args: -- diff -- hr -- podinfo -- -n -- podinfo -- --path -- tests/testdata/cluster/ -- --limit-bytes -- '20000' -stdout: '' diff --git a/tests/tool/testdata/diff_hr_not_found.yaml b/tests/tool/testdata/diff_hr_not_found.yaml deleted file mode 100644 index 39bbdbdc..00000000 --- a/tests/tool/testdata/diff_hr_not_found.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- diff -- hr -- example -- --path -- tests/testdata/cluster -stdout: | - HelmRelease object 'example' not found in 'flux-system' namespace diff --git a/tests/tool/testdata/diff_hr_not_found_all_namespaces.yaml b/tests/tool/testdata/diff_hr_not_found_all_namespaces.yaml deleted file mode 100644 index de982b20..00000000 --- a/tests/tool/testdata/diff_hr_not_found_all_namespaces.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- diff -- hr -- example -- --all-namespaces -- --path -- tests/testdata/cluster -stdout: | - HelmRelease object 'example' not found in 'None' namespace diff --git a/tests/tool/testdata/diff_hr_not_found_namespace.yaml b/tests/tool/testdata/diff_hr_not_found_namespace.yaml deleted file mode 100644 index 2c14d491..00000000 --- a/tests/tool/testdata/diff_hr_not_found_namespace.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- diff -- hr -- --namespace -- example -- --path -- tests/testdata/cluster -stdout: | - no HelmRelease objects found in 'example' namespace diff --git a/tests/tool/testdata/diff_hr_yaml.yaml b/tests/tool/testdata/diff_hr_yaml.yaml deleted file mode 100644 index e4492872..00000000 --- a/tests/tool/testdata/diff_hr_yaml.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- diff -- hr -- podinfo -- -n -- podinfo -- --path -- tests/testdata/cluster/ -- -o -- yaml diff --git a/tests/tool/testdata/diff_ks.yaml b/tests/tool/testdata/diff_ks.yaml deleted file mode 100644 index f1145a2b..00000000 --- a/tests/tool/testdata/diff_ks.yaml +++ /dev/null @@ -1,6 +0,0 @@ -args: -- diff -- ks -- apps -- --path -- tests/testdata/cluster/ diff --git a/tests/tool/testdata/diff_ks_external.yaml b/tests/tool/testdata/diff_ks_external.yaml deleted file mode 100644 index 7fcd0833..00000000 --- a/tests/tool/testdata/diff_ks_external.yaml +++ /dev/null @@ -1,8 +0,0 @@ -env: - DIFF: diff -args: -- diff -- ks -- apps -- --path -- tests/testdata/cluster/ diff --git a/tests/tool/testdata/diff_ks_yaml.yaml b/tests/tool/testdata/diff_ks_yaml.yaml deleted file mode 100644 index c05eb37c..00000000 --- a/tests/tool/testdata/diff_ks_yaml.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- diff -- ks -- apps -- --path -- tests/testdata/cluster/ -- -o -- yaml - diff --git a/tests/tool/testdata/diff_ks_yaml_empty_sources.yaml b/tests/tool/testdata/diff_ks_yaml_empty_sources.yaml deleted file mode 100644 index 2a4cdc38..00000000 --- a/tests/tool/testdata/diff_ks_yaml_empty_sources.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- diff -- ks -- apps -- --path -- tests/testdata/cluster/ -- -o -- yaml -- --sources -- '' diff --git a/tests/tool/testdata/diff_ks_yaml_limit.yaml b/tests/tool/testdata/diff_ks_yaml_limit.yaml deleted file mode 100644 index 8414ba9c..00000000 --- a/tests/tool/testdata/diff_ks_yaml_limit.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- diff -- ks -- apps -- --path -- tests/testdata/cluster/ -- -o -- yaml -- --limit-bytes -- '20000' diff --git a/tests/tool/testdata/get_cluster.yaml b/tests/tool/testdata/get_cluster.yaml deleted file mode 100644 index ae58d0ba..00000000 --- a/tests/tool/testdata/get_cluster.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster/ -stdout: | - NAME PATH KUSTOMIZATIONS - flux-system ./tests/testdata/cluster/clusters/prod 4 diff --git a/tests/tool/testdata/get_cluster2.yaml b/tests/tool/testdata/get_cluster2.yaml deleted file mode 100644 index eb4c1e39..00000000 --- a/tests/tool/testdata/get_cluster2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster2/ -stdout: | - NAME PATH KUSTOMIZATIONS - cluster tests/testdata/cluster2 5 diff --git a/tests/tool/testdata/get_cluster3.yaml b/tests/tool/testdata/get_cluster3.yaml deleted file mode 100644 index 7424ffc8..00000000 --- a/tests/tool/testdata/get_cluster3.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster3/ -- --sources -- cluster=tests/testdata/cluster3 -stdout: | - NAME PATH KUSTOMIZATIONS - cluster tests/testdata/cluster3 2 diff --git a/tests/tool/testdata/get_cluster4.yaml b/tests/tool/testdata/get_cluster4.yaml deleted file mode 100644 index 43984827..00000000 --- a/tests/tool/testdata/get_cluster4.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster4/ -stdout: | - NAME PATH KUSTOMIZATIONS - cluster tests/testdata/cluster4 3 diff --git a/tests/tool/testdata/get_cluster6.yaml b/tests/tool/testdata/get_cluster6.yaml deleted file mode 100644 index 291742d8..00000000 --- a/tests/tool/testdata/get_cluster6.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster6/ -stdout: | - NAME PATH KUSTOMIZATIONS - flux-system ./tests/testdata/cluster6/cluster 2 diff --git a/tests/tool/testdata/get_cluster7.yaml b/tests/tool/testdata/get_cluster7.yaml deleted file mode 100644 index db33d0db..00000000 --- a/tests/tool/testdata/get_cluster7.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster7/ -stdout: | - NAME PATH KUSTOMIZATIONS - flux-system ./tests/testdata/cluster7/clusters/home 3 diff --git a/tests/tool/testdata/get_cluster_all.yaml b/tests/tool/testdata/get_cluster_all.yaml deleted file mode 100644 index 7f3bb4a4..00000000 --- a/tests/tool/testdata/get_cluster_all.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- get -- cluster -- --all-namespaces -- --path -- tests/testdata/cluster/ -stdout: | - NAMESPACE NAME PATH KUSTOMIZATIONS - flux-system flux-system ./tests/testdata/cluster/clusters/prod 4 diff --git a/tests/tool/testdata/get_cluster_yaml.yaml b/tests/tool/testdata/get_cluster_yaml.yaml deleted file mode 100644 index 817edea6..00000000 --- a/tests/tool/testdata/get_cluster_yaml.yaml +++ /dev/null @@ -1,70 +0,0 @@ -args: -- get -- cluster -- --path -- tests/testdata/cluster/ -- -o -- yaml -stdout: | - --- - clusters: - - name: flux-system - namespace: flux-system - path: ./tests/testdata/cluster/clusters/prod - kustomizations: - - name: apps - namespace: flux-system - path: tests/testdata/cluster/apps/prod - helm_repos: [] - helm_releases: - - name: podinfo - namespace: podinfo - chart: - name: podinfo - repo_name: podinfo - repo_namespace: flux-system - cluster_policies: [] - - name: flux-system - namespace: flux-system - path: tests/testdata/cluster/clusters/prod - helm_repos: [] - helm_releases: [] - cluster_policies: [] - - name: infra-configs - namespace: flux-system - path: tests/testdata/cluster/infrastructure/configs - helm_repos: - - name: bitnami - namespace: flux-system - url: https://charts.bitnami.com/bitnami - repo_type: default - - name: podinfo - namespace: flux-system - url: oci://ghcr.io/stefanprodan/charts - repo_type: oci - - name: weave-charts - namespace: flux-system - url: oci://ghcr.io/weaveworks/charts - repo_type: oci - helm_releases: [] - cluster_policies: - - name: test-allow-policy - namespace: null - - name: infra-controllers - namespace: flux-system - path: tests/testdata/cluster/infrastructure/controllers - helm_repos: [] - helm_releases: - - name: weave-gitops - namespace: flux-system - chart: - name: weave-gitops - repo_name: weave-charts - repo_namespace: flux-system - - name: metallb - namespace: metallb - chart: - name: metallb - repo_name: bitnami - repo_namespace: flux-system - cluster_policies: [] diff --git a/tests/tool/testdata/get_hr.yaml b/tests/tool/testdata/get_hr.yaml deleted file mode 100644 index e338934e..00000000 --- a/tests/tool/testdata/get_hr.yaml +++ /dev/null @@ -1,11 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - podinfo podinfo 6.3.2 podinfo-podinfo podinfo - flux-system weave-gitops 4.0.22 flux-system-weave-gitops weave-charts - metallb metallb 4.1.14 metallb-metallb bitnami diff --git a/tests/tool/testdata/get_hr2.yaml b/tests/tool/testdata/get_hr2.yaml deleted file mode 100644 index 1ddbec24..00000000 --- a/tests/tool/testdata/get_hr2.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster2 -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - networking ingress-nginx 4.5.2 networking-ingress-nginx ingress-nginx - monitoring kubernetes-dashboard 6.0.0 monitoring-kubernetes-dashboard kubernetes-dashboard diff --git a/tests/tool/testdata/get_hr3.yaml b/tests/tool/testdata/get_hr3.yaml deleted file mode 100644 index d6c17b8f..00000000 --- a/tests/tool/testdata/get_hr3.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster3 -stdout: | - no HelmRelease objects found in cluster diff --git a/tests/tool/testdata/get_hr4.yaml b/tests/tool/testdata/get_hr4.yaml deleted file mode 100644 index 118d92c1..00000000 --- a/tests/tool/testdata/get_hr4.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster4 -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - monitoring kubernetes-dashboard 6.0.0 monitoring-kubernetes-dashboard kubernetes-dashboard diff --git a/tests/tool/testdata/get_hr5.yaml b/tests/tool/testdata/get_hr5.yaml deleted file mode 100644 index 0faed139..00000000 --- a/tests/tool/testdata/get_hr5.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster5 -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - flux-system weave-gitops 4.0.22 flux-system-weave-gitops weave-charts - metallb metallb 4.1.14 metallb-metallb bitnami diff --git a/tests/tool/testdata/get_hr6.yaml b/tests/tool/testdata/get_hr6.yaml deleted file mode 100644 index a8885454..00000000 --- a/tests/tool/testdata/get_hr6.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster6 -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - default renovate None default-renovate renovate diff --git a/tests/tool/testdata/get_hr7.yaml b/tests/tool/testdata/get_hr7.yaml deleted file mode 100644 index 69882234..00000000 --- a/tests/tool/testdata/get_hr7.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- get -- hr -- -A -- --path -- tests/testdata/cluster7 -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - database postgresql 12.7.1 database-postgresql bitnami-charts diff --git a/tests/tool/testdata/get_hr_name.yaml b/tests/tool/testdata/get_hr_name.yaml deleted file mode 100644 index 4524f347..00000000 --- a/tests/tool/testdata/get_hr_name.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- hr -- metallb -- -A -- --path -- tests/testdata/cluster -stdout: | - NAMESPACE NAME REVISION CHART SOURCE - metallb metallb 4.1.14 metallb-metallb bitnami diff --git a/tests/tool/testdata/get_hr_namespace.yaml b/tests/tool/testdata/get_hr_namespace.yaml deleted file mode 100644 index 7a9c0b3a..00000000 --- a/tests/tool/testdata/get_hr_namespace.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- hr -- -n -- metallb -- --path -- tests/testdata/cluster -stdout: | - NAME REVISION CHART SOURCE - metallb 4.1.14 metallb-metallb bitnami diff --git a/tests/tool/testdata/get_ks.yaml b/tests/tool/testdata/get_ks.yaml deleted file mode 100644 index 46c85838..00000000 --- a/tests/tool/testdata/get_ks.yaml +++ /dev/null @@ -1,11 +0,0 @@ -args: -- get -- ks -- --path -- tests/testdata/cluster -stdout: | - NAME PATH - apps tests/testdata/cluster/apps/prod - flux-system tests/testdata/cluster/clusters/prod - infra-configs tests/testdata/cluster/infrastructure/configs - infra-controllers tests/testdata/cluster/infrastructure/controllers diff --git a/tests/tool/testdata/get_ks2.yaml b/tests/tool/testdata/get_ks2.yaml deleted file mode 100644 index c178a996..00000000 --- a/tests/tool/testdata/get_ks2.yaml +++ /dev/null @@ -1,12 +0,0 @@ -args: -- get -- ks -- --path -- tests/testdata/cluster2 -stdout: | - NAME PATH - cluster tests/testdata/cluster2/flux - cluster-apps tests/testdata/cluster2/apps - cluster-apps-ingress-nginx tests/testdata/cluster2/apps/networking/ingress-nginx/app - cluster-apps-ingress-nginx-certificates tests/testdata/cluster2/apps/networking/ingress-nginx/certificates - cluster-apps-kubernetes-dashboard tests/testdata/cluster2/apps/monitoring/kubernetes-dashboard/app diff --git a/tests/tool/testdata/get_ks3.yaml b/tests/tool/testdata/get_ks3.yaml deleted file mode 100644 index 64ed465a..00000000 --- a/tests/tool/testdata/get_ks3.yaml +++ /dev/null @@ -1,12 +0,0 @@ -args: -- get -- ks -- -A -- --path -- tests/testdata/cluster3 -- --sources -- cluster=tests/testdata/cluster3 -stdout: | - NAMESPACE NAME PATH - flux-system namespaces tests/testdata/cluster3/namespaces/overlays/cluster3 - flux-system tenants tests/testdata/cluster3/tenants/overlays/cluster3 diff --git a/tests/tool/testdata/get_ks4.yaml b/tests/tool/testdata/get_ks4.yaml deleted file mode 100644 index b529beec..00000000 --- a/tests/tool/testdata/get_ks4.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- ks -- --path -- tests/testdata/cluster4 -stdout: | - NAME PATH - cluster tests/testdata/cluster4/flux - cluster-apps tests/testdata/cluster4/apps - cluster-apps-kubernetes-dashboard tests/testdata/cluster4/apps/monitoring/kubernetes-dashboard diff --git a/tests/tool/testdata/get_ks5.yaml b/tests/tool/testdata/get_ks5.yaml deleted file mode 100644 index 78b376d2..00000000 --- a/tests/tool/testdata/get_ks5.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- ks -- --path -- tests/testdata/cluster5 -stdout: | - NAME PATH - flux-system tests/testdata/cluster5/clusters/prod diff --git a/tests/tool/testdata/get_ks5_all.yaml b/tests/tool/testdata/get_ks5_all.yaml deleted file mode 100644 index b56dd9cc..00000000 --- a/tests/tool/testdata/get_ks5_all.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- ks -- -A -- --path -- tests/testdata/cluster5 -stdout: | - NAMESPACE NAME PATH - controllers infra-controllers tests/testdata/cluster5 - flux-system flux-system tests/testdata/cluster5/clusters/prod diff --git a/tests/tool/testdata/get_ks6.yaml b/tests/tool/testdata/get_ks6.yaml deleted file mode 100644 index 2d5db8e1..00000000 --- a/tests/tool/testdata/get_ks6.yaml +++ /dev/null @@ -1,9 +0,0 @@ -args: -- get -- ks -- --path -- tests/testdata/cluster6 -stdout: | - NAME PATH - apps tests/testdata/cluster6/apps - flux-system tests/testdata/cluster6/cluster diff --git a/tests/tool/testdata/get_ks7.yaml b/tests/tool/testdata/get_ks7.yaml deleted file mode 100644 index 0c851daa..00000000 --- a/tests/tool/testdata/get_ks7.yaml +++ /dev/null @@ -1,10 +0,0 @@ -args: -- get -- ks -- --path -- tests/testdata/cluster7 -stdout: | - NAME PATH - apps tests/testdata/cluster7/flux/apps - charts tests/testdata/cluster7/flux/charts - flux-system tests/testdata/cluster7/clusters/home diff --git a/tests/tool/testdata/get_ks_path.yaml b/tests/tool/testdata/get_ks_path.yaml deleted file mode 100644 index 6240dec9..00000000 --- a/tests/tool/testdata/get_ks_path.yaml +++ /dev/null @@ -1,11 +0,0 @@ -args: -- get -- ks -- --path -- ./tests/testdata/cluster/clusters/prod -stdout: | - NAME PATH - apps tests/testdata/cluster/apps/prod - flux-system tests/testdata/cluster/clusters/prod - infra-configs tests/testdata/cluster/infrastructure/configs - infra-controllers tests/testdata/cluster/infrastructure/controllers diff --git a/tests/tool/testdata/get_ks_path_ks.yaml b/tests/tool/testdata/get_ks_path_ks.yaml deleted file mode 100644 index 7b0eb956..00000000 --- a/tests/tool/testdata/get_ks_path_ks.yaml +++ /dev/null @@ -1,8 +0,0 @@ -args: -- get -- ks -- --all-namespaces -- --path -- ./tests/testdata/cluster/apps/prod -stdout: | - no Kustomization objects found in cluster diff --git a/tests/tool/testdata/get_ks_wide.yaml b/tests/tool/testdata/get_ks_wide.yaml deleted file mode 100644 index 2c938c2c..00000000 --- a/tests/tool/testdata/get_ks_wide.yaml +++ /dev/null @@ -1,13 +0,0 @@ -args: -- get -- ks -- -o -- wide -- --path -- tests/testdata/cluster -stdout: | - NAME PATH HELMREPOS RELEASES - apps tests/testdata/cluster/apps/prod 0 1 - flux-system tests/testdata/cluster/clusters/prod 0 0 - infra-configs tests/testdata/cluster/infrastructure/configs 3 0 - infra-controllers tests/testdata/cluster/infrastructure/controllers 0 2 diff --git a/tests/tool/testdata/test_hr.yaml b/tests/tool/testdata/test_hr.yaml deleted file mode 100644 index 57466b98..00000000 --- a/tests/tool/testdata/test_hr.yaml +++ /dev/null @@ -1,4 +0,0 @@ -args: -- test -- --enable-helm -- tests/testdata/cluster diff --git a/tests/tool/testdata/test_hr2.yaml b/tests/tool/testdata/test_hr2.yaml deleted file mode 100644 index 9dd41cdb..00000000 --- a/tests/tool/testdata/test_hr2.yaml +++ /dev/null @@ -1,4 +0,0 @@ -args: -- test -- --enable-helm -- tests/testdata/cluster2 diff --git a/tests/tool/testdata/test_hr3.yaml b/tests/tool/testdata/test_hr3.yaml deleted file mode 100644 index 98fb8b82..00000000 --- a/tests/tool/testdata/test_hr3.yaml +++ /dev/null @@ -1,6 +0,0 @@ -args: -- test -- --enable-helm -- --sources -- cluster=tests/testdata/cluster3 -- tests/testdata/cluster3 diff --git a/tests/tool/testdata/test_hr_policy.yaml b/tests/tool/testdata/test_hr_policy.yaml deleted file mode 100644 index 65ff8803..00000000 --- a/tests/tool/testdata/test_hr_policy.yaml +++ /dev/null @@ -1,5 +0,0 @@ -args: -- test -- --enable-helm -- --enable-kyverno -- tests/testdata/cluster2 diff --git a/tests/tool/testdata/test_hr_policy2.yaml b/tests/tool/testdata/test_hr_policy2.yaml deleted file mode 100644 index eace179e..00000000 --- a/tests/tool/testdata/test_hr_policy2.yaml +++ /dev/null @@ -1,5 +0,0 @@ -args: -- test -- --enable-helm -- --enable-kyverno -- tests/testdata/cluster diff --git a/tests/tool/testdata/test_ks.yaml b/tests/tool/testdata/test_ks.yaml deleted file mode 100644 index 7e3c5ebd..00000000 --- a/tests/tool/testdata/test_ks.yaml +++ /dev/null @@ -1,3 +0,0 @@ -args: -- test -- tests/testdata/cluster/