diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..194519976 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,34 @@ +version: 2 + +updates: + - package-ecosystem: "gomod" + directory: "/" + labels: ["dependencies"] + schedule: + interval: "monthly" + groups: + go-deps: + patterns: + - "*" + allow: + - dependency-type: "direct" + ignore: + # Kubernetes deps are updated by fluxcd/pkg + - dependency-name: "k8s.io/*" + - dependency-name: "sigs.k8s.io/*" + # KMS SDKs are updated by SOPS + - dependency-name: "github.com/Azure/*" + - dependency-name: "github.com/aws/*" + - dependency-name: "github.com/hashicorp/vault/*" + # Flux APIs pkg are updated at release time + - dependency-name: "github.com/fluxcd/kustomize-controller/api" + - dependency-name: "github.com/fluxcd/source-controller/api" + - package-ecosystem: "github-actions" + directory: "/" + labels: ["area/ci", "dependencies"] + groups: + ci: + patterns: + - "*" + schedule: + interval: "monthly" diff --git a/.github/labels.yaml b/.github/labels.yaml new file mode 100644 index 000000000..7e7241b47 --- /dev/null +++ b/.github/labels.yaml @@ -0,0 +1,40 @@ +# Configuration file to declaratively configure labels +# Ref: https://github.com/EndBug/label-sync#Config-files + +- name: area/kustomize + description: Kustomize related issues and pull requests + color: '#00e54d' +- name: area/kstatus + description: Health checking related issues and pull requests + color: '#25D5CA' + aliases: ['area/health-checks'] +- name: area/sops + description: SOPS related issues and pull requests + color: '#FEE5D1' +- name: area/server-side-apply + description: SSA related issues and pull requests + color: '#2819CB' +- name: area/varsub + description: Post-build variable substitution related issues and pull requests + color: '#8D195D' +- name: backport:release/v1.0.x + description: To be backported to release/v1.0.x + color: '#ffd700' +- name: backport:release/v1.1.x + description: To be backported to release/v1.1.x + color: '#ffd700' +- name: backport:release/v1.2.x + description: To be backported to release/v1.2.x + color: '#ffd700' +- name: backport:release/v1.3.x + description: To be backported to release/v1.3.x + color: '#ffd700' +- name: backport:release/v1.4.x + description: To be backported to release/v1.4.x + color: '#ffd700' +- name: backport:release/v1.5.x + description: To be backported to release/v1.5.x + color: '#ffd700' +- name: backport:release/v1.6.x + description: To be backported to release/v1.6.x + color: '#ffd700' diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml new file mode 100644 index 000000000..3616da2f6 --- /dev/null +++ b/.github/workflows/backport.yaml @@ -0,0 +1,34 @@ +name: backport + +on: + pull_request_target: + types: [closed, labeled] + +permissions: + contents: read + +jobs: + pull-request: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name)) + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Create backport PRs + uses: korthout/backport-action@436145e922f9561fc5ea157ff406f21af2d6b363 # v3.2.0 + # xref: https://github.com/korthout/backport-action#inputs + with: + # Use token to allow workflows to be triggered for the created PR + github_token: ${{ secrets.BOT_GITHUB_TOKEN }} + # Match labels with a pattern `backport:` + label_pattern: '^backport:([^ ]+)$' + # A bit shorter pull-request title than the default + pull_title: '[${target_branch}] ${pull_title}' + # Simpler PR description than default + pull_description: |- + Automated backport to `${target_branch}`, triggered by a label in #${pull_number}. diff --git a/.github/workflows/cifuzz.yaml b/.github/workflows/cifuzz.yaml index 4971aaef1..3f8d7aade 100644 --- a/.github/workflows/cifuzz.yaml +++ b/.github/workflows/cifuzz.yaml @@ -12,20 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: 1.19.x - - id: go-env - run: | - echo "::set-output name=go-mod-cache::$(go env GOMODCACHE)" - - name: Restore Go cache - uses: actions/cache@v3 - with: - path: ${{ steps.go-env.outputs.go-mod-cache }} - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go + go-version: 1.24.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Smoke test Fuzzers run: make fuzz-smoketest diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index fd0866935..f86fec969 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -4,7 +4,8 @@ on: pull_request: push: branches: - - main + - 'main' + - 'release/**' permissions: contents: read # for actions/checkout to fetch code @@ -14,21 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 - - name: Restore Go cache - uses: actions/cache@v3 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: cache with: path: /tmp/.buildx-cache @@ -36,16 +30,20 @@ jobs: restore-keys: | ${{ runner.os }}-buildx-ghcache- - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: 1.19.x + go-version: 1.24.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Setup Kubernetes - uses: helm/kind-action@v1.5.0 + uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0 with: - version: v0.17.0 + version: v0.20.0 cluster_name: kind + node_image: kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72 - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main + uses: fluxcd/pkg/actions/kustomize@main - name: Enable integration tests # Only run integration tests for main branch if: github.ref == 'refs/heads/main' @@ -103,7 +101,7 @@ jobs: - name: Run tests for removing kubectl managed fields run: | kubectl create ns managed-fields - kustomize build github.com/stefanprodan/podinfo//kustomize?ref=6.0.0 > /tmp/podinfo.yaml + kustomize build github.com/stefanprodan/podinfo//kustomize?ref=6.3.5 > /tmp/podinfo.yaml kubectl -n managed-fields apply -f /tmp/podinfo.yaml kubectl -n managed-fields apply -f ./config/testdata/managed-fields kubectl -n managed-fields wait kustomization/podinfo --for=condition=ready --timeout=4m diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 720cb1c5f..c92dba3f5 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -9,23 +9,22 @@ env: permissions: contents: read # for actions/checkout to fetch code - + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: all + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 with: buildkitd-flags: "--debug" - name: Build multi-arch container image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: push: false builder: ${{ steps.buildx.outputs.name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index adaab1f78..06c76894a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,18 +11,25 @@ on: required: true permissions: - contents: write # needed to write releases - id-token: write # needed for keyless signing - packages: write # needed for ghcr access + contents: read env: CONTROLLER: ${{ github.event.repository.name }} jobs: - build-push: + release: + outputs: + hashes: ${{ steps.slsa.outputs.hashes }} + image_url: ${{ steps.slsa.outputs.image_url }} + image_digest: ${{ steps.slsa.outputs.image_digest }} runs-on: ubuntu-latest + permissions: + contents: write # for creating the GitHub release. + id-token: write # for creating OIDC tokens for signing. + packages: write # for pushing and signing container images. steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@main - name: Prepare @@ -35,24 +42,24 @@ jobs: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT - name: Setup QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: fluxcdbot password: ${{ secrets.GHCR_TOKEN }} - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: fluxcdbot password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} - name: Generate images meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: | fluxcd/${{ env.CONTROLLER }} @@ -60,7 +67,8 @@ jobs: tags: | type=raw,value=${{ steps.prep.outputs.VERSION }} - name: Publish images - uses: docker/build-push-action@v3 + id: build-push + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: sbom: true provenance: true @@ -71,32 +79,82 @@ jobs: platforms: linux/amd64,linux/arm/v7,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - - name: Check images - run: | - docker buildx imagetools inspect docker.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }} - docker buildx imagetools inspect ghcr.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }} - docker pull docker.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }} - docker pull ghcr.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }} - - uses: sigstore/cosign-installer@main + - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - name: Sign images env: COSIGN_EXPERIMENTAL: 1 run: | - cosign sign fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }} - cosign sign ghcr.io/fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.VERSION }} + cosign sign --yes fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }} + cosign sign --yes ghcr.io/fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }} - name: Generate release artifacts if: startsWith(github.ref, 'refs/tags/v') run: | mkdir -p config/release kustomize build ./config/crd > ./config/release/${{ env.CONTROLLER }}.crds.yaml kustomize build ./config/manager > ./config/release/${{ env.CONTROLLER }}.deployment.yaml - echo '[CHANGELOG](https://github.com/fluxcd/${{ env.CONTROLLER }}/blob/main/CHANGELOG.md)' > ./config/release/notes.md - - uses: anchore/sbom-action/download-syft@v0 + - uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0 - name: Create release and SBOM + id: run-goreleaser if: startsWith(github.ref, 'refs/tags/v') - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: version: latest - args: release --release-notes=config/release/notes.md --rm-dist --skip-validate + args: release --clean --skip=validate env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Generate SLSA metadata + id: slsa + env: + ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" + run: | + hashes=$(echo $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) + echo "hashes=$hashes" >> $GITHUB_OUTPUT + + image_url=fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.version }} + echo "image_url=$image_url" >> $GITHUB_OUTPUT + + image_digest=${{ steps.build-push.outputs.digest }} + echo "image_digest=$image_digest" >> $GITHUB_OUTPUT + + release-provenance: + needs: [release] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + contents: write # for uploading attestations to GitHub releases. + if: startsWith(github.ref, 'refs/tags/v') + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + provenance-name: "provenance.intoto.jsonl" + base64-subjects: "${{ needs.release.outputs.hashes }}" + upload-assets: true + + dockerhub-provenance: + needs: [release] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + packages: write # for uploading attestations. + if: startsWith(github.ref, 'refs/tags/v') + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 + with: + image: ${{ needs.release.outputs.image_url }} + digest: ${{ needs.release.outputs.image_digest }} + registry-username: fluxcdbot + secrets: + registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} + + ghcr-provenance: + needs: [release] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + packages: write # for uploading attestations. + if: startsWith(github.ref, 'refs/tags/v') + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 + with: + image: ghcr.io/${{ needs.release.outputs.image_url }} + digest: ${{ needs.release.outputs.image_digest }} + registry-username: fluxcdbot + secrets: + registry-password: ${{ secrets.GHCR_TOKEN }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index b8d9b44e8..1d19e0adb 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -11,15 +11,16 @@ on: permissions: contents: read # for actions/checkout to fetch code security-events: write # for codeQL to write security events - + jobs: fossa: name: FOSSA runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run FOSSA scan and upload build data - uses: fossa-contrib/fossa-action@v1 + uses: fossa-contrib/fossa-action@3d2ef181b1820d6dcd1972f86a767d18167fa19b # v3.0.1 with: # FOSSA Push-Only API Token fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de @@ -29,17 +30,23 @@ jobs: name: CodeQL runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: 1.19.x + go-version: 1.24.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: go + # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # xref: https://codeql.github.com/codeql-query-help/go/ + queries: security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 diff --git a/.github/workflows/sync-labels.yaml b/.github/workflows/sync-labels.yaml new file mode 100644 index 000000000..d0c2c8816 --- /dev/null +++ b/.github/workflows/sync-labels.yaml @@ -0,0 +1,28 @@ +name: sync-labels +on: + workflow_dispatch: + push: + branches: + - main + paths: + - .github/labels.yaml + +permissions: + contents: read + +jobs: + labels: + name: Run sync + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 + with: + # Configuration file + config-file: | + https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml + .github/labels.yaml + # Strictly declarative + delete-other-labels: true diff --git a/.gitignore b/.gitignore index 8e2deb9cd..fcf0a7877 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,26 @@ -# Binaries for programs and plugins +# Binaries for programs and plugins. *.exe *.exe~ *.dll *.so *.dylib -# Test binary, built with `go test -c` +# Test binary, built with `go test -c`. *.test -# Output of the go coverage tool, specifically when used with LiteIDE +# Output of the go coverage tool. *.out -# Dependency directories (remove the comment below to include it) -# vendor/ +# Build tools downloaded at runtime. bin/ + +# Release manifests generated at runtime. config/release/ config/crd/bases/ocirepositories.yaml config/crd/bases/gitrepositories.yaml config/crd/bases/buckets.yaml build/ + +# CRDs for fuzzing tests. +internal/controllers/testdata/crd diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9d887aa6d..f9e8d6932 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -4,9 +4,26 @@ builds: - skip: true release: - prerelease: "true" extra_files: - glob: config/release/*.yaml + prerelease: "auto" + header: | + ## Changelog + + [{{.Tag}} changelog](https://github.com/fluxcd/{{.ProjectName}}/blob/{{.Tag}}/CHANGELOG.md) + footer: | + ## Container images + + - `docker.io/fluxcd/{{.ProjectName}}:{{.Tag}}` + - `ghcr.io/fluxcd/{{.ProjectName}}:{{.Tag}}` + + Supported architectures: `linux/amd64`, `linux/arm64` and `linux/arm/v7`. + + The container images are built on GitHub hosted runners and are signed with cosign and GitHub OIDC. + To verify the images and their provenance (SLSA level 3), please see the [security documentation](https://fluxcd.io/flux/security/). + +changelog: + disable: true checksum: extra_files: @@ -32,6 +49,7 @@ signs: certificate: "${artifact}.pem" args: - sign-blob + - "--yes" - "--output-certificate=${certificate}" - "--output-signature=${signature}" - "${artifact}" diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc5b0819..42864fa7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,646 @@ All notable changes to this project are documented in this file. +## 1.6.0 + +**Release date:** 2025-05-28 + +This minor release comes with various bug fixes and improvements. + +Kustomization API now supports object-level workload identity by setting +`.spec.decryption.serviceAccountName` to the name of a service account +in the same namespace that has been configured with appropriate cloud +permissions. For this feature to work, the controller feature gate +`ObjectLevelWorkloadIdentity` must be enabled. See a complete guide +[here](https://fluxcd.io/flux/integrations/). + +Kustomization API now supports the value `WaitForTermination` for the +`.spec.deletionPolicy` field. This instructs the controller to wait for the +deletion of all resources managed by the Kustomization before allowing the +Kustomization itself to be deleted. See docs +[here](https://fluxcd.io/flux/components/kustomize/kustomizations/#deletion-policy). + +In addition, the Kubernetes dependencies have been updated to v1.33 and +various other controller dependencies have been updated to their latest version. +The controller is now built with Go 1.24. + +Fixes: +- Fix performance regression due to using client without cache + [#1436](https://github.com/fluxcd/kustomize-controller/pull/1436) +- Fix secret value showing up in logs + [#1372](https://github.com/fluxcd/kustomize-controller/pull/1372) + +Improvements: +- [RFC-0010] Introduce KMS provider decryption with service account + [#1426](https://github.com/fluxcd/kustomize-controller/pull/1426) + [#1449](https://github.com/fluxcd/kustomize-controller/pull/1449) + [#1456](https://github.com/fluxcd/kustomize-controller/pull/1456) +- Add `WaitForTermination` option to DeletionPolicy + [#1444](https://github.com/fluxcd/kustomize-controller/pull/1444) +- Skip emitting events for suspended Kustomizations + [#1396](https://github.com/fluxcd/kustomize-controller/pull/1396) +- Various dependency updates + [#1458](https://github.com/fluxcd/kustomize-controller/pull/1458) + [#1448](https://github.com/fluxcd/kustomize-controller/pull/1448) + [#1433](https://github.com/fluxcd/kustomize-controller/pull/1433) + [#1435](https://github.com/fluxcd/kustomize-controller/pull/1435) + [#1429](https://github.com/fluxcd/kustomize-controller/pull/1429) + [#1414](https://github.com/fluxcd/kustomize-controller/pull/1414) + [#1410](https://github.com/fluxcd/kustomize-controller/pull/1410) + [#1401](https://github.com/fluxcd/kustomize-controller/pull/1401) + +## 1.5.1 + +**Release date:** 2025-02-25 + +This patch release fixes a bug introduced in v1.5.0 +that was causing spurious logging for deprecated API versions +and sometimes failures on health checks. + +In addition, all error logs resulting from SOPS decryption +failures have been sanitised. + +Fixes: +- Fix secret value showing up in logs + [#1372](https://github.com/fluxcd/kustomize-controller/pull/1372) +- Use lazy restmapper vendored from controller-runtime v0.19 + [#1377](https://github.com/fluxcd/kustomize-controller/pull/1377) + +## 1.5.0 + +**Release date:** 2025-02-18 + +This minor release comes with various bug fixes and improvements. + +The controller has been updated to Kustomize **v5.6**, please see the +`kubernetes-sigs/kustomize` [changelog](https://github.com/kubernetes-sigs/kustomize/releases) +for more details. + +The Kustomization API now supports custom health checks for Custom +Resources through Common Expression Language (CEL) expressions. +See [docs](https://fluxcd.io/flux/components/kustomize/kustomizations/#health-check-expressions). + +The controller now sends an origin revision from OCI artifact +annotations to notification-controller on events, which is +useful for updating commit statuses on the notification +providers that support this feature. +See [docs](https://fluxcd.io/flux/cheatsheets/oci-artifacts/#git-commit-status-updates). + +It is now also possible to control whether or not kustomize-controller +will orphan resources when a Kustomization is deleted. +See [docs](https://fluxcd.io/flux/components/kustomize/kustomizations/#deletion-policy). + +In addition, the Kubernetes dependencies have been updated to v1.32.1 and +various other controller dependencies have been updated to their latest +version. + +Fixes: +- Clarify precedence in Kustomization substituteFrom + [#1301](https://github.com/fluxcd/kustomize-controller/pull/1301) +- Remove deprecated object metrics from controllers + [#1305](https://github.com/fluxcd/kustomize-controller/pull/1305) + +Improvements: +- Enable decryption of secrets generated by Kustomize components + [#1283](https://github.com/fluxcd/kustomize-controller/pull/1283) +- Added decryption of Kustomize patches and refactor SOPS tests + [#1286](https://github.com/fluxcd/kustomize-controller/pull/1286) +- Allow control of finalization garbage collection + [#1314](https://github.com/fluxcd/kustomize-controller/pull/1314) +- Add OCI revision to events + [#1338](https://github.com/fluxcd/kustomize-controller/pull/1338) +- [RFC-0009] Add CEL custom healthchecks + [#1344](https://github.com/fluxcd/kustomize-controller/pull/1344) +- Add GroupChangeLog feature gate to fix es indexing cardinality + [#1361](https://github.com/fluxcd/kustomize-controller/pull/1361) +- Various dependency updates + [#1302](https://github.com/fluxcd/kustomize-controller/pull/1302) + [#1304](https://github.com/fluxcd/kustomize-controller/pull/1304) + [#1310](https://github.com/fluxcd/kustomize-controller/pull/1310) + [#1313](https://github.com/fluxcd/kustomize-controller/pull/1313) + [#1318](https://github.com/fluxcd/kustomize-controller/pull/1318) + [#1320](https://github.com/fluxcd/kustomize-controller/pull/1320) + [#1330](https://github.com/fluxcd/kustomize-controller/pull/1330) + [#1348](https://github.com/fluxcd/kustomize-controller/pull/1348) + [#1352](https://github.com/fluxcd/kustomize-controller/pull/1352) + [#1354](https://github.com/fluxcd/kustomize-controller/pull/1354) + [#1359](https://github.com/fluxcd/kustomize-controller/pull/1359) + [#1362](https://github.com/fluxcd/kustomize-controller/pull/1362) + [#1364](https://github.com/fluxcd/kustomize-controller/pull/1364) + [#1358](https://github.com/fluxcd/kustomize-controller/pull/1358) + +## 1.4.0 + +**Release date:** 2024-09-27 + +This minor release comes with various bug fixes and improvements. + +kustomize-controller in [sharded +deployment](https://fluxcd.io/flux/installation/configuration/sharding/) +configuration now supports cross-shard dependency check. This allows a +Kustomization to depend on other Kustomizations managed by different controller +shards. + +In addition, the Kubernetes dependencies have been updated to v1.31.1 and +various other controller dependencies have been updated to their latest version. +The controller is now built with Go 1.23. + +Fixes: +- Fix incorrect use of format strings with the conditions package. + [#1198](https://github.com/fluxcd/kustomize-controller/pull/1198) + +Improvements: +- Update Bucket API to v1 + [#1253](https://github.com/fluxcd/kustomize-controller/pull/1253) +- Allow cross-shard dependency check + [#1248](https://github.com/fluxcd/kustomize-controller/pull/1248) +- docs: Clarify .spec.decryption.secretRef usage + [#1242](https://github.com/fluxcd/kustomize-controller/pull/1242) +- Build with Go 1.23 + [#1230](https://github.com/fluxcd/kustomize-controller/pull/1230) +- Various dependency updates + [#1165](https://github.com/fluxcd/kustomize-controller/pull/1165) + [#1181](https://github.com/fluxcd/kustomize-controller/pull/1181) + [#1212](https://github.com/fluxcd/kustomize-controller/pull/1212) + [#1228](https://github.com/fluxcd/kustomize-controller/pull/1228) + [#1229](https://github.com/fluxcd/kustomize-controller/pull/1229) + [#1233](https://github.com/fluxcd/kustomize-controller/pull/1233) + [#1239](https://github.com/fluxcd/kustomize-controller/pull/1239) + [#1240](https://github.com/fluxcd/kustomize-controller/pull/1240) + [#1243](https://github.com/fluxcd/kustomize-controller/pull/1243) + [#1249](https://github.com/fluxcd/kustomize-controller/pull/1249) + [#1250](https://github.com/fluxcd/kustomize-controller/pull/1250) + [#1251](https://github.com/fluxcd/kustomize-controller/pull/1251) + +## 1.3.0 + +**Release date:** 2024-05-06 + +This minor release comes with new features, improvements and bug fixes. + +The controller has been updated to Kustomize **v5.4**, please see the +`kubernetes-sigs/kustomize` [changelog](https://github.com/kubernetes-sigs/kustomize/releases) +for more details. + +The Flux `Kustomization` API gains two optional fields `.spec.namePrefix` and `.spec.nameSuffix` +that can be used to specify a prefix and suffix to be added to the names +of all managed resources. + +The controller now supports the `--feature-gates=StrictPostBuildSubstitutions=true` +flag, when enabled the post-build substitutions will fail if a +variable without a default value is declared in files but is +missing from the input vars. + +When using variable substitution with values that are numbers or booleans, +it is now possible to covert the values to strings, for more details see the +[post-build documentation](https://github.com/fluxcd/kustomize-controller/blob/release/v1.3.x/docs/spec/v1/kustomizations.md#post-build-substitution-of-numbers-and-booleans). + +In addition, the controller dependencies have been updated to Kubernetes v1.30 +and controller-runtime v0.18. Various other dependencies have also been updated to +their latest version to patch upstream CVEs. + +Lastly, the controller is now built with Go 1.22. + +Improvements: +- Implement name prefix/suffix transformers + [#1134](https://github.com/fluxcd/kustomize-controller/pull/1134) +- Add `StrictPostBuildSubstitutions` feature flag + [#1130](https://github.com/fluxcd/kustomize-controller/pull/1130) +- Document how to use numbers and booleans in post build substitutions + [#1129](https://github.com/fluxcd/kustomize-controller/pull/1129) +- Remove deprecated aad pod identity from API docs + [#1152](https://github.com/fluxcd/kustomize-controller/pull/1152) +- api: Refer condition type constants from `fluxcd/pkg/apis` + [#1144](https://github.com/fluxcd/kustomize-controller/pull/1144) +- Update dependencies to Kustomize v5.4.0 + [#1128](https://github.com/fluxcd/kustomize-controller/pull/1128) +- Various dependency updates + [#1155](https://github.com/fluxcd/kustomize-controller/pull/1155) + [#1121](https://github.com/fluxcd/kustomize-controller/pull/1121) + [#1139](https://github.com/fluxcd/kustomize-controller/pull/1139) + [#1122](https://github.com/fluxcd/kustomize-controller/pull/1122) + +Fixes: +- Fix requeue warning introduced by controller-runtime + [#1090](https://github.com/fluxcd/kustomize-controller/pull/1090) +- Remove effectless statement + [#1091](https://github.com/fluxcd/kustomize-controller/pull/1091) +- Remove `genclient:Namespaced` tag + [#1092](https://github.com/fluxcd/kustomize-controller/pull/1092) + +## 1.2.2 + +**Release date:** 2024-02-01 + +This patch release comes with various bug fixes and improvements. + +Reconciling empty directories and directories without Kubernetes manifests no +longer results in an error. This regressing bug was introduced with the +controller upgrade to Kustomize v5.3 and has been fixed in this patch release. + +The regression due to which the namespaced objects without a namespace specified +resulted in `not found` error instead of `namespace not specified` has also been +fixed. And the regression due to which Roles and ClusterRoles were reconciled +over and over due to the normalization of Roles and ClusterRoles has also been +fixed. + +In addition, the Kubernetes dependencies have been updated to v1.28.6. Various +other dependencies have also been updated to their latest version to patch +upstream CVEs. + +Lastly, the controller is now built with Go 1.21. + +Improvements: +- Update Go to 1.21 + [#1053](https://github.com/fluxcd/kustomize-controller/pull/1053) +- Various dependency updates + [#1076](https://github.com/fluxcd/kustomize-controller/pull/1076) + [#1074](https://github.com/fluxcd/kustomize-controller/pull/1074) + [#1070](https://github.com/fluxcd/kustomize-controller/pull/1070) + [#1068](https://github.com/fluxcd/kustomize-controller/pull/1068) + [#1065](https://github.com/fluxcd/kustomize-controller/pull/1065) + [#1060](https://github.com/fluxcd/kustomize-controller/pull/1060) + [#1059](https://github.com/fluxcd/kustomize-controller/pull/1059) + [#1051](https://github.com/fluxcd/kustomize-controller/pull/1051) + [#1049](https://github.com/fluxcd/kustomize-controller/pull/1049) + [#1046](https://github.com/fluxcd/kustomize-controller/pull/1046) + [#1044](https://github.com/fluxcd/kustomize-controller/pull/1044) + [#1040](https://github.com/fluxcd/kustomize-controller/pull/1040) + [#1038](https://github.com/fluxcd/kustomize-controller/pull/1038) + +## 1.2.1 + +**Release date:** 2023-12-14 + +This patch release comes with improvements in logging to provide faster feedback +on any HTTP errors encountered while fetching source artifacts. + +In addition, the status condition messages are now trimmed to respect the size +limit defined by the API. + +Improvements: +- Update runtime to v0.43.3 + [#1031](https://github.com/fluxcd/kustomize-controller/pull/1031) +- Log HTTP errors to provide faster feedback + [#1028](https://github.com/fluxcd/kustomize-controller/pull/1028) + +## 1.2.0 + +**Release date:** 2023-12-11 + +This minor release comes with performance improvements, bug fixes and several new features. + +The controller has been updated from Kustomize v5.0 to **v5.3**, please the see +`kubernetes-sigs/kustomize` [changelog](https://github.com/kubernetes-sigs/kustomize/releases) +for a more details. + +Starting with this version, the controller will automatically perform a cleanup of +the Pods belonging to stale Kubernetes Jobs after a force apply. + +A new controller flag `--override-manager` has been added to extend the Field Managers disallow list. +Using this flag, cluster administrators can configure the controller to undo changes +made with Lens and other UI tools that directly modify Kubernetes objects on clusters. + +In addition, the controller dependencies have been updated, including an update to Kubernetes v1.28. +The container base image has been updated to Alpine 3.19. + +Improvements: +- Update source-controller to v1.2.2 + [#1024](https://github.com/fluxcd/kustomize-controller/pull/1024) +- build: update Alpine to 3.19 + [#1023](https://github.com/fluxcd/kustomize-controller/pull/1023) +- Update Kustomize to v5.3.0 + [#1021](https://github.com/fluxcd/kustomize-controller/pull/1021) +- Support additional Field Managers in the disallow list + [#1017](https://github.com/fluxcd/kustomize-controller/pull/1017) +- Add test for Namespace custom resource + [#1016](https://github.com/fluxcd/kustomize-controller/pull/1016) +- Update controller to Kubernetes v1.28.4 + [#1014](https://github.com/fluxcd/kustomize-controller/pull/1014) +- Disable status poller cache by default + [#1012](https://github.com/fluxcd/kustomize-controller/pull/1012) +- Tweak permissions on various created files + [#1005](https://github.com/fluxcd/kustomize-controller/pull/1005) +- Cleanup pods when recreating Kubernetes Jobs + [#997](https://github.com/fluxcd/kustomize-controller/pull/997) +- Update SOPS to v3.8.1 + [#995](https://github.com/fluxcd/kustomize-controller/pull/995) + +## 1.1.1 + +**Release date:** 2023-10-11 + +This patch release contains an improvement to retry the reconciliation of a +`Kustomization` as soon as the source artifact is available in storage. +Which is particularly useful when the source-controller has just been upgraded. + +In addition, the controller can now detect immutable field errors returned by the +Google Cloud k8s-config-connector admission controller and recreate the GCP custom +resources annotated with `kustomize.toolkit.fluxcd.io/force: Enabled`. + +Improvements: +- Update `fluxcd/pkg` dependencies + [#983](https://github.com/fluxcd/kustomize-controller/pull/983) +- Bump `github.com/cyphar/filepath-securejoi`n from 0.2.3 to 0.2.4 + [#962](https://github.com/fluxcd/kustomize-controller/pull/962) + +Fixes: +- fix: Retry when artifacts are available in storage + [#980](https://github.com/fluxcd/kustomize-controller/pull/980) +- fix: Consistent artifact fetching retry timing + [#978](https://github.com/fluxcd/kustomize-controller/pull/978) + +## 1.1.0 + +**Release date:** 2023-08-23 + +This minor release comes with performance improvements, bug fixes and several new features. + +The apply behaviour has been extended with two policies `IfNotPresent` and `Ignore`. +To change the apply behaviour for specific Kubernetes resources, you can annotate them with: + +| Annotation | Default | Values | Role | +|-------------------------------------|------------|----------------------------------------------------------------|-----------------| +| `kustomize.toolkit.fluxcd.io/ssa` | `Override` | - `Override`
- `Merge`
- `IfNotPresent`
- `Ignore` | Apply policy | +| `kustomize.toolkit.fluxcd.io/force` | `Disabled` | - `Enabled`
- `Disabled` | Recreate policy | +| `kustomize.toolkit.fluxcd.io/prune` | `Enabled` | - `Enabled`
- `Disabled` | Delete policy | + +The `IfNotPresent` policy instructs the controller to only apply the Kubernetes resources if they are not present on the cluster. +This policy can be used for Kubernetes `Secrets` and `ValidatingWebhookConfigurations` managed by cert-manager, +where Flux creates the resources with fields that are later on mutated by other controllers. + +This version improves the health checking with fail-fast behaviour +by detecting stalled Kubernetes rollouts. + +In addition, the controller now stops exporting an object's +metrics as soon as the object has been deleted. + +Lastly, this release introduces two controller flags: + +- The `--concurrent-ssa` flag sets the number of concurrent server-side apply operations + performed by the controller. Defaults to 4 concurrent operations per reconciliation. +- The `--interval-jitter-percentage` flag makes the + controller distribute the load more evenly when multiple objects are set up + with the same interval. The default of this flag is set to `5`, which means + that the interval will be jittered by a +/- 5% random value (e.g. if the + interval is 10 minutes, the actual reconciliation interval will be between 9.5 + and 10.5 minutes). + +Improvements: +- Add `--concurrent-ssa` flag + [#948](https://github.com/fluxcd/kustomize-controller/pull/948) +- Add `IfNotPresent` and `Ignore` SSA policies + [#943](https://github.com/fluxcd/kustomize-controller/pull/943) +- controller: jitter requeue interval + [#940](https://github.com/fluxcd/kustomize-controller/pull/940) +- Enable fail-fast behavior for health checks + [#933](https://github.com/fluxcd/kustomize-controller/pull/933) +- Bump `fluxcd/pkg/ssa` to improve immutable error detection + [#932](https://github.com/fluxcd/kustomize-controller/pull/932) +- Update dependencies + [#939](https://github.com/fluxcd/kustomize-controller/pull/939) +- Update Source API to v1.1.0 + [#952](https://github.com/fluxcd/kustomize-controller/pull/952) + +Fixes: +- Handle delete before adding finalizer + [#930](https://github.com/fluxcd/kustomize-controller/pull/930) +- Delete stale metrics on object delete + [#944](https://github.com/fluxcd/kustomize-controller/pull/944) + +## 1.0.1 + +**Release date:** 2023-07-10 + +This is a patch release that fixes spurious events emitted for skipped resources. + +Fixes: +- Exclude skipped resources from apply events + [#920](https://github.com/fluxcd/kustomize-controller/pull/920) + +## 1.0.0 + +**Release date:** 2023-07-04 + +This is the first stable release of the controller. From now on, this controller +follows the [Flux 2 release cadence and support pledge](https://fluxcd.io/flux/releases/). + +Starting with this version, the build, release and provenance portions of the +Flux project supply chain [provisionally meet SLSA Build Level 3](https://fluxcd.io/flux/security/slsa-assessment/). + +This release includes several bug fixes. In addition, dependencies have been updated +to their latest version, including an update of Kubernetes to v1.27.3. + +For a comprehensive list of changes since `v0.35.x`, please refer to the +changelog for [v1.0.0-rc.1](#100-rc1), [v1.0.0-rc.2](#100-rc2), +[v1.0.0-rc.3](#100-rc3) and [`v1.0.0-rc.4](#100-rc4). + +Improvements: +- Update dependencies + [#908](https://github.com/fluxcd/kustomize-controller/pull/908) +- Align `go.mod` version with Kubernetes (Go 1.20) + [#900](https://github.com/fluxcd/kustomize-controller/pull/900) + +Fixes: +- Use kustomization namespace for empty dependency source namespace + [#897](https://github.com/fluxcd/kustomize-controller/pull/897) +- docs: Clarify that targetNamespace namespace can be part of resources + [#896](https://github.com/fluxcd/kustomize-controller/pull/896) + +## 1.0.0-rc.4 + +**Release date:** 2023-05-29 + +This release candidate comes with support for Kustomize v5.0.3. + +⚠️ Note that Kustomize v5 contains breaking changes, please consult their +[changelog](https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.0.0) +for more details. + +In addition, the controller dependencies have been updated to +Kubernetes v1.27.2 and controller-runtime v0.15.0. + +Improvements: +- Update Kubernetes to v1.27 and Kustomize to v5 + [#850](https://github.com/fluxcd/kustomize-controller/pull/850) +- Update controller-runtime to v0.15.0 + [#869](https://github.com/fluxcd/kustomize-controller/pull/869) +- Update CA certificates + [#872](https://github.com/fluxcd/kustomize-controller/pull/872) +- Update source-controller to v1.0.0-rc.4 + [#873](https://github.com/fluxcd/kustomize-controller/pull/873) + +## 1.0.0-rc.3 + +**Release date:** 2023-05-12 + +This release candidate comes with improved error reporting for when +the controller fails to fetch an artifact due to a checksum mismatch. + +In addition, the controller dependencies have been updated to patch +CVE-2023-1732 and the base image has been updated to Alpine 3.18. + +Improvements: +- Update Alpine to 3.18 + [#855](https://github.com/fluxcd/kustomize-controller/pull/855) +- Update dependencies + [#862](https://github.com/fluxcd/kustomize-controller/pull/862) +- build(deps): bump github.com/cloudflare/circl from 1.1.0 to 1.3.3 + [#860](https://github.com/fluxcd/kustomize-controller/pull/860) +- docs: Clarify the Kustomize components relative paths requirement + [#861](https://github.com/fluxcd/kustomize-controller/pull/861) + +## 1.0.0-rc.2 + +**Release date:** 2023-05-09 + +This release candidate fixes secrets decryption when using Azure Key Vault. + +In addition, the controller dependencies have been updated to their latest +versions. + +Improvements: +- Fix SOPS azkv envCred + [#838](https://github.com/fluxcd/kustomize-controller/pull/838) +- Update dependencies + [#853](https://github.com/fluxcd/kustomize-controller/pull/853) + +## 1.0.0-rc.1 + +**Release date:** 2023-04-03 + +This release candidate promotes the `Kustomization` API from `v1beta2` to `v1`. +The controller now supports horizontal scaling using +sharding based on a label selector. + +In addition, the controller now supports Workload Identity when +decrypting secrets with SOPS and Azure Vault. + +### Highlights + +This release candidate requires the `GitRepository` API version `v1`, +first shipped with [source-controller](https://github.com/fluxcd/source-controller) +v1.0.0-rc.1. + +#### API changes + +The `Kustomization` kind was promoted from v1beta2 to v1 (GA) and deprecated fields were removed. + +A new optional field called `CommonMetadata` was added to the API +for setting labels and/or annotations to all resources part of a Kustomization. +The main difference to the Kustomize +[commonLabels](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonlabels/) and +[commonAnnotations](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonannotations/), +is that the controller sets the labels and annotations only to the top level `metadata` field, +without patching the Kubernetes Deployment `spec.template` or the Service `spec.selector`. + +The `kustomizations.kustomize.toolkit.fluxcd.io` CRD contains the following versions: +- v1 (storage version) +- v1beta2 (deprecated) +- v1beta1 (deprecated) + +#### Upgrade procedure + +The `Kustomization` v1 API is backwards compatible with v1beta2, except for the following: +- the deprecated field `.spec.validation` was removed +- the deprecated field `.spec.patchesStrategicMerge` was removed (replaced by `.spec.patches`) +- the deprecated field `.spec.patchesJson6902 ` was removed (replaced by `.spec.patches`) + +To upgrade from v1beta2, after deploying the new CRD and controller, +set `apiVersion: kustomize.toolkit.fluxcd.io/v1` in the YAML files that contain +`Kustomization` definitions and remove the deprecated fields if any. +Bumping the API version in manifests can be done gradually. +It is advised to not delay this procedure as the beta versions will be removed after 6 months. + +#### Sharding + +Starting with this release, the controller can be configured with +`--watch-label-selector`, after which only objects with this label will +be reconciled by the controller. + +This allows for horizontal scaling, where kustomize-controller +can be deployed multiple times with a unique label selector +which is used as the sharding key. + +### Full changelog + +Improvements: +- GA: Promote Kustomization API to `kustomize.toolkit.fluxcd.io/v1` + [#822](https://github.com/fluxcd/kustomize-controller/pull/822) +- Add common labels and annotations patching capabilities + [#817](https://github.com/fluxcd/kustomize-controller/pull/817) +- Add reconciler sharding capability based on label selector + [#821](https://github.com/fluxcd/kustomize-controller/pull/821) +- Support Workload Identity for Azure Vault + [#813](https://github.com/fluxcd/kustomize-controller/pull/813) +- Verify Digest of Artifact + [#818](https://github.com/fluxcd/kustomize-controller/pull/818) +- Move `controllers` to `internal/controllers` + [#820](https://github.com/fluxcd/kustomize-controller/pull/820) +- build(deps): bump github.com/opencontainers/runc from 1.1.2 to 1.1.5 + [#824](https://github.com/fluxcd/kustomize-controller/pull/824) + +## 0.35.1 + +**Release date:** 2023-03-20 + +This prerelease comes with a fix to error reporting. +The controller will now reveal validation errors when force applying +resources with immutable field changes. + +In addition, the controller dependencies have been updated to their latest +versions. + +Improvements: +- Update dependencies + [#814](https://github.com/fluxcd/kustomize-controller/pull/814) + +## 0.35.0 + +**Release date:** 2023-03-08 + +This prerelease adds support for disabling the cache of the `kstatus` status +poller, which is used to determine the health of the resources applied by the +controller. To disable the cache, configure the Deployment of the controller +with `--feature-gates=DisableStatusPollerCache=true`. + +This may have a positive impact on memory usage on large clusters with many +objects, at the cost of an increased number of API calls. + +In addition, `klog` has been configured to log using the same logger as the +rest of the controller (providing a consistent log format). + +Lastly, the controller is now built using Go `1.20`, and the dependencies have +been updated to their latest versions. + +Improvements: +- api: update description LastAppliedRevision + [#798](https://github.com/fluxcd/kustomize-controller/pull/798) +- Update Go to 1.20 + [#806](https://github.com/fluxcd/kustomize-controller/pull/806) +- Update dependencies + [#807](https://github.com/fluxcd/kustomize-controller/pull/807) + [#811](https://github.com/fluxcd/kustomize-controller/pull/811) +- Use `logger.SetLogger` to also configure `klog` + [#809](https://github.com/fluxcd/kustomize-controller/pull/809) + +## 0.34.0 + +**Release date:** 2023-02-17 + +This prerelease adds support for parsing the +[RFC-0005](https://github.com/fluxcd/flux2/tree/main/rfcs/0005-artifact-revision-and-digest) +revision format produced by source-controller `>=v0.35.0`. + +In addition, the controller dependencies have been updated to their latest +versions. + +Improvements: +- Support RFC-0005 revision format + [#793](https://github.com/fluxcd/kustomize-controller/pull/793) +- Update dependencies + [#796](https://github.com/fluxcd/kustomize-controller/pull/796) + ## 0.33.0 **Release date:** 2023-02-01 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 5df028d85..8756f4f14 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -13,19 +13,10 @@ There are a number of dependencies required to be able to run the controller and - [Install Docker](https://docs.docker.com/engine/install/) - (Optional) [Install Kubebuilder](https://book.kubebuilder.io/quick-start.html#installation) -In addition to the above, the following dependencies are also used by some of the `make` targets: - -- `controller-gen` (v0.7.0) -- `gen-crd-api-reference-docs` (v0.3.0) -- `setup-envtest` (latest) -- `sops` (v3.7.2) - -If any of the above dependencies are not present on your system, the first invocation of a `make` target that requires them will install them. - ## How to run the test suite Prerequisites: -* Go >= 1.18 +* Go >= 1.24 You can run the test suite by simply doing diff --git a/Dockerfile b/Dockerfile index db277915e..6a2ed3b5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -ARG GO_VERSION=1.19 -ARG XX_VERSION=1.1.0 +ARG GO_VERSION=1.24 +ARG XX_VERSION=1.6.1 FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine as builder +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS builder # Copy the build utilities. COPY --from=xx / / @@ -24,18 +24,18 @@ RUN go mod download # copy source code COPY main.go main.go -COPY controllers/ controllers/ COPY internal/ internal/ # build ENV CGO_ENABLED=0 RUN xx-go build -trimpath -a -o kustomize-controller main.go -FROM alpine:3.17 +FROM alpine:3.21 -# Uses GnuPG from edge to patch CVE-2022-3515. -RUN apk add --no-cache ca-certificates tini git openssh-client && \ - apk add --no-cache gnupg --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main +ARG TARGETPLATFORM + +RUN apk --no-cache add ca-certificates tini git openssh-client gnupg \ + && update-ca-certificates COPY --from=builder /workspace/kustomize-controller /usr/local/bin/ diff --git a/Makefile b/Makefile index cd550b206..883b88dab 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ CRD_OPTIONS ?= crd:crdVersions=v1 SOURCE_VER ?= $(shell go list -m all | grep github.com/fluxcd/source-controller/api | awk '{print $$2}') # Use the same version of SOPS already referenced on go.mod -SOPS_VER := $(shell go list -m all | grep go.mozilla.org/sops | awk '{print $$2}') +SOPS_VER := $(shell go list -m all | grep github.com/getsops/sops | awk '{print $$2}') # Repository root based on Git metadata REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel) @@ -43,6 +43,10 @@ OCIREPO_CRD ?= config/crd/bases/ocirepositories.yaml # detect and download new CRDs when the SOURCE_VER changes. SOURCE_CRD_VER=$(BUILD_DIR)/.src-crd-$(SOURCE_VER) +# API (doc) generation utilities +CONTROLLER_GEN_VERSION ?= v0.16.1 +GEN_API_REF_DOCS_VERSION ?= e327d0730470cbd61b06300f81c5fcf91c23c113 + all: manager # Download the envtest binaries to testbin @@ -54,7 +58,7 @@ install-envtest: setup-envtest SOPS = $(GOBIN)/sops $(SOPS): ## Download latest sops binary if none is found. - $(call go-install-tool,$(SOPS),go.mozilla.org/sops/v3/cmd/sops@$(SOPS_VER)) + $(call go-install-tool,$(SOPS),github.com/getsops/sops/v3/cmd/sops@$(SOPS_VER)) # Run controller tests KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" @@ -74,6 +78,7 @@ run: generate fmt vet manifests $(SOURCE_CRD_VER): rm -f $(BUILD_DIR)/.src-crd* $(MAKE) cleanup-crd-deps + if ! test -d "$(BUILD_DIR)"; then mkdir -p $(BUILD_DIR); fi touch $(SOURCE_CRD_VER) $(GITREPO_CRD): @@ -126,12 +131,12 @@ manifests: controller-gen # Generate API reference documentation api-docs: gen-crd-api-reference-docs - $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/kustomize.md + $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v1/kustomize.md # Run go mod tidy tidy: - cd api; rm -f go.sum; go mod tidy -compat=1.19 - rm -f go.sum; go mod tidy -compat=1.19 + cd api; rm -f go.sum; go mod tidy -compat=1.24 + rm -f go.sum; go mod tidy -compat=1.24 # Run go fmt against code fmt: @@ -166,13 +171,13 @@ docker-deploy: CONTROLLER_GEN = $(GOBIN)/controller-gen .PHONY: controller-gen controller-gen: ## Download controller-gen locally if necessary. - $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION)) # Find or download gen-crd-api-reference-docs GEN_CRD_API_REFERENCE_DOCS = $(GOBIN)/gen-crd-api-reference-docs .PHONY: gen-crd-api-reference-docs -gen-crd-api-reference-docs: - $(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@v0.3.0) +gen-crd-api-reference-docs: ## Download gen-crd-api-reference-docs locally if necessary + $(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@$(GEN_API_REF_DOCS_VERSION)) ENVTEST = $(GOBIN)/setup-envtest .PHONY: envtest diff --git a/PROJECT b/PROJECT index 97afa1d1b..4d16c7e83 100644 --- a/PROJECT +++ b/PROJECT @@ -1,6 +1,9 @@ domain: toolkit.fluxcd.io repo: github.com/fluxcd/kustomize-controller resources: +- group: kustomize + kind: Kustomization + version: v1 - group: kustomize kind: Kustomization version: v1beta2 diff --git a/README.md b/README.md index d79a193ab..4658cc9f8 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,14 @@ the controller performs actions to reconcile the cluster current state with the ## Specifications -* [API](docs/spec/v1beta2/README.md) +* [API](docs/spec/v1/README.md) * [Controller](docs/spec/README.md) ## Guides * [Get started with Flux](https://fluxcd.io/flux/get-started/) * [Setup Notifications](https://fluxcd.io/flux/guides/notifications/) -* [Manage Kubernetes secrets with Flux and Mozilla SOPS](https://fluxcd.io/flux/guides/mozilla-sops/) +* [Manage Kubernetes secrets with Flux and SOPS](https://fluxcd.io/flux/guides/mozilla-sops/) * [How to build, publish and consume OCI Artifacts with Flux](https://fluxcd.io/flux/cheatsheets/oci-artifacts/) * [Flux and Kustomize FAQ](https://fluxcd.io/flux/faq/#kustomize-questions) diff --git a/api/go.mod b/api/go.mod index 900430d21..acb03112f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,36 +1,36 @@ module github.com/fluxcd/kustomize-controller/api -go 1.18 +go 1.24.0 require ( - github.com/fluxcd/pkg/apis/kustomize v0.8.0 - github.com/fluxcd/pkg/apis/meta v0.19.0 - k8s.io/apiextensions-apiserver v0.26.1 - k8s.io/apimachinery v0.26.1 - sigs.k8s.io/controller-runtime v0.14.2 + github.com/fluxcd/pkg/apis/kustomize v1.10.0 + github.com/fluxcd/pkg/apis/meta v1.12.0 + k8s.io/apiextensions-apiserver v0.33.0 + k8s.io/apimachinery v0.33.0 + sigs.k8s.io/controller-runtime v0.21.0 ) -// Fix CVE-2022-32149 -replace golang.org/x/text => golang.org/x/text v0.4.0 - // Fix CVE-2022-28948 replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 require ( - github.com/go-logr/logr v1.2.3 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect - golang.org/x/text v0.5.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.80.1 // indirect - k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/api/go.sum b/api/go.sum index a8dc1c4c4..8bf56af00 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,79 +1,87 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fluxcd/pkg/apis/kustomize v0.8.0 h1:A6aLolxPV2Sll44SOHiX96lbXXmRZmS5BoEerkRHrfM= -github.com/fluxcd/pkg/apis/kustomize v0.8.0/go.mod h1:9DPEVSfVIkiC2H3Dk6Ght4YJkswhYIaufXla4tB5Y84= -github.com/fluxcd/pkg/apis/meta v0.19.0 h1:CX75e/eaRWZDTzNdMSWomY1InlssLKcS8GQDSg/aopI= -github.com/fluxcd/pkg/apis/meta v0.19.0/go.mod h1:7b6prDPsViyAzoY7eRfSPS0/MbXpGGsOMvRq2QrTKa4= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/fluxcd/pkg/apis/kustomize v1.10.0 h1:47EeSzkQvlQZdH92vHMe2lK2iR8aOSEJq95avw5idts= +github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ= +github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg= +github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= -github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc= -golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -83,24 +91,26 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ= -k8s.io/apiextensions-apiserver v0.26.1 h1:cB8h1SRk6e/+i3NOrQgSFij1B2S0Y0wDoNl66bn8RMI= -k8s.io/apiextensions-apiserver v0.26.1/go.mod h1:AptjOSXDGuE0JICx/Em15PaoO7buLwTs0dGleIHixSM= -k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ= -k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.14.2 h1:P6IwDhbsRWsBClt/8/h8Zy36bCuGuW5Op7MHpFrN/60= -sigs.k8s.io/controller-runtime v0.14.2/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/api/v1/doc.go b/api/v1/doc.go new file mode 100644 index 000000000..fa70fe1d2 --- /dev/null +++ b/api/v1/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the kustomize.toolkit.fluxcd.io +// v1 API group. +// +kubebuilder:object:generate=true +// +groupName=kustomize.toolkit.fluxcd.io +package v1 diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go new file mode 100644 index 000000000..e8f6b39fd --- /dev/null +++ b/api/v1/groupversion_info.go @@ -0,0 +1,33 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "kustomize.toolkit.fluxcd.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1/inventory_types.go b/api/v1/inventory_types.go new file mode 100644 index 000000000..69e52146c --- /dev/null +++ b/api/v1/inventory_types.go @@ -0,0 +1,34 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +// ResourceInventory contains a list of Kubernetes resource object references +// that have been applied by a Kustomization. +type ResourceInventory struct { + // Entries of Kubernetes resource object references. + Entries []ResourceRef `json:"entries"` +} + +// ResourceRef contains the information necessary to locate a resource within a cluster. +type ResourceRef struct { + // ID is the string representation of the Kubernetes resource object's metadata, + // in the format '___'. + ID string `json:"id"` + + // Version is the API version of the Kubernetes resource object's kind. + Version string `json:"v"` +} diff --git a/api/v1/kustomization_types.go b/api/v1/kustomization_types.go new file mode 100644 index 000000000..4854aaa92 --- /dev/null +++ b/api/v1/kustomization_types.go @@ -0,0 +1,381 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "time" + + "github.com/fluxcd/pkg/apis/kustomize" + "github.com/fluxcd/pkg/apis/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + KustomizationKind = "Kustomization" + KustomizationFinalizer = "finalizers.fluxcd.io" + MaxConditionMessageLength = 20000 + EnabledValue = "enabled" + DisabledValue = "disabled" + MergeValue = "Merge" + IfNotPresentValue = "IfNotPresent" + IgnoreValue = "Ignore" + + DeletionPolicyMirrorPrune = "MirrorPrune" + DeletionPolicyDelete = "Delete" + DeletionPolicyWaitForTermination = "WaitForTermination" + DeletionPolicyOrphan = "Orphan" +) + +// KustomizationSpec defines the configuration to calculate the desired state +// from a Source using Kustomize. +type KustomizationSpec struct { + // CommonMetadata specifies the common labels and annotations that are + // applied to all resources. Any existing label or annotation will be + // overridden if its key matches a common one. + // +optional + CommonMetadata *CommonMetadata `json:"commonMetadata,omitempty"` + + // DependsOn may contain a meta.NamespacedObjectReference slice + // with references to Kustomization resources that must be ready before this + // Kustomization can be reconciled. + // +optional + DependsOn []meta.NamespacedObjectReference `json:"dependsOn,omitempty"` + + // Decrypt Kubernetes secrets before applying them on the cluster. + // +optional + Decryption *Decryption `json:"decryption,omitempty"` + + // The interval at which to reconcile the Kustomization. + // This interval is approximate and may be subject to jitter to ensure + // efficient use of resources. + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + // +required + Interval metav1.Duration `json:"interval"` + + // The interval at which to retry a previously failed reconciliation. + // When not specified, the controller uses the KustomizationSpec.Interval + // value to retry failures. + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + // +optional + RetryInterval *metav1.Duration `json:"retryInterval,omitempty"` + + // The KubeConfig for reconciling the Kustomization on a remote cluster. + // When used in combination with KustomizationSpec.ServiceAccountName, + // forces the controller to act on behalf of that Service Account at the + // target cluster. + // If the --default-service-account flag is set, its value will be used as + // a controller level fallback for when KustomizationSpec.ServiceAccountName + // is empty. + // +optional + KubeConfig *meta.KubeConfigReference `json:"kubeConfig,omitempty"` + + // Path to the directory containing the kustomization.yaml file, or the + // set of plain YAMLs a kustomization.yaml should be generated for. + // Defaults to 'None', which translates to the root path of the SourceRef. + // +optional + Path string `json:"path,omitempty"` + + // PostBuild describes which actions to perform on the YAML manifest + // generated by building the kustomize overlay. + // +optional + PostBuild *PostBuild `json:"postBuild,omitempty"` + + // Prune enables garbage collection. + // +required + Prune bool `json:"prune"` + + // DeletionPolicy can be used to control garbage collection when this + // Kustomization is deleted. Valid values are ('MirrorPrune', 'Delete', + // 'WaitForTermination', 'Orphan'). 'MirrorPrune' mirrors the Prune field + // (orphan if false, delete if true). Defaults to 'MirrorPrune'. + // +kubebuilder:validation:Enum=MirrorPrune;Delete;WaitForTermination;Orphan + // +optional + DeletionPolicy string `json:"deletionPolicy,omitempty"` + + // A list of resources to be included in the health assessment. + // +optional + HealthChecks []meta.NamespacedObjectKindReference `json:"healthChecks,omitempty"` + + // NamePrefix will prefix the names of all managed resources. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=200 + // +kubebuilder:validation:Optional + // +optional + NamePrefix string `json:"namePrefix,omitempty" yaml:"namePrefix,omitempty"` + + // NameSuffix will suffix the names of all managed resources. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=200 + // +kubebuilder:validation:Optional + // +optional + NameSuffix string `json:"nameSuffix,omitempty" yaml:"nameSuffix,omitempty"` + + // Strategic merge and JSON patches, defined as inline YAML objects, + // capable of targeting objects based on kind, label and annotation selectors. + // +optional + Patches []kustomize.Patch `json:"patches,omitempty"` + + // Images is a list of (image name, new name, new tag or digest) + // for changing image names, tags or digests. This can also be achieved with a + // patch, but this operator is simpler to specify. + // +optional + Images []kustomize.Image `json:"images,omitempty"` + + // The name of the Kubernetes service account to impersonate + // when reconciling this Kustomization. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // Reference of the source where the kustomization file is. + // +required + SourceRef CrossNamespaceSourceReference `json:"sourceRef"` + + // This flag tells the controller to suspend subsequent kustomize executions, + // it does not apply to already started executions. Defaults to false. + // +optional + Suspend bool `json:"suspend,omitempty"` + + // TargetNamespace sets or overrides the namespace in the + // kustomization.yaml file. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Optional + // +optional + TargetNamespace string `json:"targetNamespace,omitempty"` + + // Timeout for validation, apply and health checking operations. + // Defaults to 'Interval' duration. + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$" + // +optional + Timeout *metav1.Duration `json:"timeout,omitempty"` + + // Force instructs the controller to recreate resources + // when patching fails due to an immutable field change. + // +kubebuilder:default:=false + // +optional + Force bool `json:"force,omitempty"` + + // Wait instructs the controller to check the health of all the reconciled + // resources. When enabled, the HealthChecks are ignored. Defaults to false. + // +optional + Wait bool `json:"wait,omitempty"` + + // Components specifies relative paths to specifications of other Components. + // +optional + Components []string `json:"components,omitempty"` + + // HealthCheckExprs is a list of healthcheck expressions for evaluating the + // health of custom resources using Common Expression Language (CEL). + // The expressions are evaluated only when Wait or HealthChecks are specified. + // +optional + HealthCheckExprs []kustomize.CustomHealthCheck `json:"healthCheckExprs,omitempty"` +} + +// CommonMetadata defines the common labels and annotations. +type CommonMetadata struct { + // Annotations to be added to the object's metadata. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + + // Labels to be added to the object's metadata. + // +optional + Labels map[string]string `json:"labels,omitempty"` +} + +// Decryption defines how decryption is handled for Kubernetes manifests. +type Decryption struct { + // Provider is the name of the decryption engine. + // +kubebuilder:validation:Enum=sops + // +required + Provider string `json:"provider"` + + // ServiceAccountName is the name of the service account used to + // authenticate with KMS services from cloud providers. If a + // static credential for a given cloud provider is defined + // inside the Secret referenced by SecretRef, that static + // credential takes priority. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // The secret name containing the private OpenPGP keys used for decryption. + // A static credential for a cloud provider defined inside the Secret + // takes priority to secret-less authentication with the ServiceAccountName + // field. + // +optional + SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` +} + +// PostBuild describes which actions to perform on the YAML manifest +// generated by building the kustomize overlay. +type PostBuild struct { + // Substitute holds a map of key/value pairs. + // The variables defined in your YAML manifests that match any of the keys + // defined in the map will be substituted with the set value. + // Includes support for bash string replacement functions + // e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}. + // +optional + Substitute map[string]string `json:"substitute,omitempty"` + + // SubstituteFrom holds references to ConfigMaps and Secrets containing + // the variables and their values to be substituted in the YAML manifests. + // The ConfigMap and the Secret data keys represent the var names, and they + // must match the vars declared in the manifests for the substitution to + // happen. + // +optional + SubstituteFrom []SubstituteReference `json:"substituteFrom,omitempty"` +} + +// SubstituteReference contains a reference to a resource containing +// the variables name and value. +type SubstituteReference struct { + // Kind of the values referent, valid values are ('Secret', 'ConfigMap'). + // +kubebuilder:validation:Enum=Secret;ConfigMap + // +required + Kind string `json:"kind"` + + // Name of the values referent. Should reside in the same namespace as the + // referring resource. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +required + Name string `json:"name"` + + // Optional indicates whether the referenced resource must exist, or whether to + // tolerate its absence. If true and the referenced resource is absent, proceed + // as if the resource was present but empty, without any variables defined. + // +kubebuilder:default:=false + // +optional + Optional bool `json:"optional,omitempty"` +} + +// KustomizationStatus defines the observed state of a kustomization. +type KustomizationStatus struct { + meta.ReconcileRequestStatus `json:",inline"` + + // ObservedGeneration is the last reconciled generation. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // The last successfully applied revision. + // Equals the Revision of the applied Artifact from the referenced Source. + // +optional + LastAppliedRevision string `json:"lastAppliedRevision,omitempty"` + + // The last successfully applied origin revision. + // Equals the origin revision of the applied Artifact from the referenced Source. + // Usually present on the Metadata of the applied Artifact and depends on the + // Source type, e.g. for OCI it's the value associated with the key + // "org.opencontainers.image.revision". + // +optional + LastAppliedOriginRevision string `json:"lastAppliedOriginRevision,omitempty"` + + // LastAttemptedRevision is the revision of the last reconciliation attempt. + // +optional + LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"` + + // Inventory contains the list of Kubernetes resource object references that + // have been successfully applied. + // +optional + Inventory *ResourceInventory `json:"inventory,omitempty"` +} + +// GetTimeout returns the timeout with default. +func (in Kustomization) GetTimeout() time.Duration { + duration := in.Spec.Interval.Duration - 30*time.Second + if in.Spec.Timeout != nil { + duration = in.Spec.Timeout.Duration + } + if duration < 30*time.Second { + return 30 * time.Second + } + return duration +} + +// GetRetryInterval returns the retry interval +func (in Kustomization) GetRetryInterval() time.Duration { + if in.Spec.RetryInterval != nil { + return in.Spec.RetryInterval.Duration + } + return in.GetRequeueAfter() +} + +// GetRequeueAfter returns the duration after which the Kustomization must be +// reconciled again. +func (in Kustomization) GetRequeueAfter() time.Duration { + return in.Spec.Interval.Duration +} + +// GetDeletionPolicy returns the deletion policy and default value if not specified. +func (in Kustomization) GetDeletionPolicy() string { + if in.Spec.DeletionPolicy == "" { + return DeletionPolicyMirrorPrune + } + return in.Spec.DeletionPolicy +} + +// GetDependsOn returns the list of dependencies across-namespaces. +func (in Kustomization) GetDependsOn() []meta.NamespacedObjectReference { + return in.Spec.DependsOn +} + +// GetConditions returns the status conditions of the object. +func (in Kustomization) GetConditions() []metav1.Condition { + return in.Status.Conditions +} + +// SetConditions sets the status conditions on the object. +func (in *Kustomization) SetConditions(conditions []metav1.Condition) { + in.Status.Conditions = conditions +} + +// +genclient +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:resource:shortName=ks +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" + +// Kustomization is the Schema for the kustomizations API. +type Kustomization struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KustomizationSpec `json:"spec,omitempty"` + // +kubebuilder:default:={"observedGeneration":-1} + Status KustomizationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// KustomizationList contains a list of kustomizations. +type KustomizationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Kustomization `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Kustomization{}, &KustomizationList{}) +} diff --git a/api/v1/reference_types.go b/api/v1/reference_types.go new file mode 100644 index 000000000..cf0d9abd2 --- /dev/null +++ b/api/v1/reference_types.go @@ -0,0 +1,48 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import "fmt" + +// CrossNamespaceSourceReference contains enough information to let you locate the +// typed Kubernetes resource object at cluster level. +type CrossNamespaceSourceReference struct { + // API version of the referent. + // +optional + APIVersion string `json:"apiVersion,omitempty"` + + // Kind of the referent. + // +kubebuilder:validation:Enum=OCIRepository;GitRepository;Bucket + // +required + Kind string `json:"kind"` + + // Name of the referent. + // +required + Name string `json:"name"` + + // Namespace of the referent, defaults to the namespace of the Kubernetes + // resource object that contains the reference. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +func (s *CrossNamespaceSourceReference) String() string { + if s.Namespace != "" { + return fmt.Sprintf("%s/%s/%s", s.Kind, s.Namespace, s.Name) + } + return fmt.Sprintf("%s/%s", s.Kind, s.Name) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..a4ccb2ff2 --- /dev/null +++ b/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,335 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "github.com/fluxcd/pkg/apis/kustomize" + "github.com/fluxcd/pkg/apis/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonMetadata) DeepCopyInto(out *CommonMetadata) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonMetadata. +func (in *CommonMetadata) DeepCopy() *CommonMetadata { + if in == nil { + return nil + } + out := new(CommonMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceSourceReference. +func (in *CrossNamespaceSourceReference) DeepCopy() *CrossNamespaceSourceReference { + if in == nil { + return nil + } + out := new(CrossNamespaceSourceReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Decryption) DeepCopyInto(out *Decryption) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(meta.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Decryption. +func (in *Decryption) DeepCopy() *Decryption { + if in == nil { + return nil + } + out := new(Decryption) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Kustomization) DeepCopyInto(out *Kustomization) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kustomization. +func (in *Kustomization) DeepCopy() *Kustomization { + if in == nil { + return nil + } + out := new(Kustomization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Kustomization) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KustomizationList) DeepCopyInto(out *KustomizationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Kustomization, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KustomizationList. +func (in *KustomizationList) DeepCopy() *KustomizationList { + if in == nil { + return nil + } + out := new(KustomizationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KustomizationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KustomizationSpec) DeepCopyInto(out *KustomizationSpec) { + *out = *in + if in.CommonMetadata != nil { + in, out := &in.CommonMetadata, &out.CommonMetadata + *out = new(CommonMetadata) + (*in).DeepCopyInto(*out) + } + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = make([]meta.NamespacedObjectReference, len(*in)) + copy(*out, *in) + } + if in.Decryption != nil { + in, out := &in.Decryption, &out.Decryption + *out = new(Decryption) + (*in).DeepCopyInto(*out) + } + out.Interval = in.Interval + if in.RetryInterval != nil { + in, out := &in.RetryInterval, &out.RetryInterval + *out = new(metav1.Duration) + **out = **in + } + if in.KubeConfig != nil { + in, out := &in.KubeConfig, &out.KubeConfig + *out = new(meta.KubeConfigReference) + **out = **in + } + if in.PostBuild != nil { + in, out := &in.PostBuild, &out.PostBuild + *out = new(PostBuild) + (*in).DeepCopyInto(*out) + } + if in.HealthChecks != nil { + in, out := &in.HealthChecks, &out.HealthChecks + *out = make([]meta.NamespacedObjectKindReference, len(*in)) + copy(*out, *in) + } + if in.Patches != nil { + in, out := &in.Patches, &out.Patches + *out = make([]kustomize.Patch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Images != nil { + in, out := &in.Images, &out.Images + *out = make([]kustomize.Image, len(*in)) + copy(*out, *in) + } + out.SourceRef = in.SourceRef + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(metav1.Duration) + **out = **in + } + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.HealthCheckExprs != nil { + in, out := &in.HealthCheckExprs, &out.HealthCheckExprs + *out = make([]kustomize.CustomHealthCheck, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KustomizationSpec. +func (in *KustomizationSpec) DeepCopy() *KustomizationSpec { + if in == nil { + return nil + } + out := new(KustomizationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KustomizationStatus) DeepCopyInto(out *KustomizationStatus) { + *out = *in + out.ReconcileRequestStatus = in.ReconcileRequestStatus + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Inventory != nil { + in, out := &in.Inventory, &out.Inventory + *out = new(ResourceInventory) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KustomizationStatus. +func (in *KustomizationStatus) DeepCopy() *KustomizationStatus { + if in == nil { + return nil + } + out := new(KustomizationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostBuild) DeepCopyInto(out *PostBuild) { + *out = *in + if in.Substitute != nil { + in, out := &in.Substitute, &out.Substitute + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.SubstituteFrom != nil { + in, out := &in.SubstituteFrom, &out.SubstituteFrom + *out = make([]SubstituteReference, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostBuild. +func (in *PostBuild) DeepCopy() *PostBuild { + if in == nil { + return nil + } + out := new(PostBuild) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceInventory) DeepCopyInto(out *ResourceInventory) { + *out = *in + if in.Entries != nil { + in, out := &in.Entries, &out.Entries + *out = make([]ResourceRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceInventory. +func (in *ResourceInventory) DeepCopy() *ResourceInventory { + if in == nil { + return nil + } + out := new(ResourceInventory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceRef) DeepCopyInto(out *ResourceRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRef. +func (in *ResourceRef) DeepCopy() *ResourceRef { + if in == nil { + return nil + } + out := new(ResourceRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubstituteReference) DeepCopyInto(out *SubstituteReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubstituteReference. +func (in *SubstituteReference) DeepCopy() *SubstituteReference { + if in == nil { + return nil + } + out := new(SubstituteReference) + in.DeepCopyInto(out) + return out +} diff --git a/api/v1beta1/kustomization_types.go b/api/v1beta1/kustomization_types.go index 49160a894..d00dbf960 100644 --- a/api/v1beta1/kustomization_types.go +++ b/api/v1beta1/kustomization_types.go @@ -271,13 +271,13 @@ const ( ) // +genclient -// +genclient:Namespaced // +kubebuilder:object:root=true // +kubebuilder:resource:shortName=ks // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" +// +kubebuilder:deprecatedversion:warning="v1beta1 Kustomization is deprecated, upgrade to v1" // Kustomization is the Schema for the kustomizations API. type Kustomization struct { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 766d61863..8edb67985 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1,8 +1,7 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* -Copyright 2021 The Flux authors +Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -176,7 +175,9 @@ func (in *KustomizationSpec) DeepCopyInto(out *KustomizationSpec) { if in.Patches != nil { in, out := &in.Patches, &out.Patches *out = make([]kustomize.Patch, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.PatchesStrategicMerge != nil { in, out := &in.PatchesStrategicMerge, &out.PatchesStrategicMerge diff --git a/api/v1beta2/kustomization_types.go b/api/v1beta2/kustomization_types.go index ecab71b1b..f19b93e09 100644 --- a/api/v1beta2/kustomization_types.go +++ b/api/v1beta2/kustomization_types.go @@ -36,6 +36,11 @@ const ( // KustomizationSpec defines the configuration to calculate the desired state from a Source using Kustomize. type KustomizationSpec struct { + // CommonMetadata specifies the common labels and annotations that are applied to all resources. + // Any existing label or annotation will be overridden if its key matches a common one. + // +optional + CommonMetadata *CommonMetadata `json:"commonMetadata,omitempty"` + // DependsOn may contain a meta.NamespacedObjectReference slice // with references to Kustomization resources that must be ready before this // Kustomization can be reconciled. @@ -150,14 +155,25 @@ type KustomizationSpec struct { // +optional Wait bool `json:"wait,omitempty"` + // Components specifies relative paths to specifications of other Components. + // +optional + Components []string `json:"components,omitempty"` + // Deprecated: Not used in v1beta2. // +kubebuilder:validation:Enum=none;client;server // +optional Validation string `json:"validation,omitempty"` +} - // Components specifies relative paths to specifications of other Components +// CommonMetadata defines the common labels and annotations. +type CommonMetadata struct { + // Annotations to be added to the object's metadata. // +optional - Components []string `json:"components,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + + // Labels to be added to the object's metadata. + // +optional + Labels map[string]string `json:"labels,omitempty"` } // Decryption defines how decryption is handled for Kubernetes manifests. @@ -227,7 +243,7 @@ type KustomizationStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty"` // The last successfully applied revision. - // The revision format for Git sources is /. + // Equals the Revision of the applied Artifact from the referenced Source. // +optional LastAppliedRevision string `json:"lastAppliedRevision,omitempty"` @@ -288,14 +304,13 @@ func (in *Kustomization) GetStatusConditions() *[]metav1.Condition { } // +genclient -// +genclient:Namespaced -// +kubebuilder:storageversion // +kubebuilder:object:root=true // +kubebuilder:resource:shortName=ks // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" +// +kubebuilder:deprecatedversion:warning="v1beta2 Kustomization is deprecated, upgrade to v1" // Kustomization is the Schema for the kustomizations API. type Kustomization struct { @@ -319,11 +334,3 @@ type KustomizationList struct { func init() { SchemeBuilder.Register(&Kustomization{}, &KustomizationList{}) } - -func trimString(str string, limit int) string { - if len(str) <= limit { - return str - } - - return str[0:limit] + "..." -} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 0ef46eab1..9ecae16b7 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -1,8 +1,7 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* -Copyright 2021 The Flux authors +Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,6 +28,35 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonMetadata) DeepCopyInto(out *CommonMetadata) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonMetadata. +func (in *CommonMetadata) DeepCopy() *CommonMetadata { + if in == nil { + return nil + } + out := new(CommonMetadata) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) { *out = *in @@ -126,6 +154,11 @@ func (in *KustomizationList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KustomizationSpec) DeepCopyInto(out *KustomizationSpec) { *out = *in + if in.CommonMetadata != nil { + in, out := &in.CommonMetadata, &out.CommonMetadata + *out = new(CommonMetadata) + (*in).DeepCopyInto(*out) + } if in.DependsOn != nil { in, out := &in.DependsOn, &out.DependsOn *out = make([]meta.NamespacedObjectReference, len(*in)) @@ -160,7 +193,9 @@ func (in *KustomizationSpec) DeepCopyInto(out *KustomizationSpec) { if in.Patches != nil { in, out := &in.Patches, &out.Patches *out = make([]kustomize.Patch, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.PatchesStrategicMerge != nil { in, out := &in.PatchesStrategicMerge, &out.PatchesStrategicMerge diff --git a/compass.yml b/compass.yml new file mode 100644 index 000000000..3830b7317 --- /dev/null +++ b/compass.yml @@ -0,0 +1,31 @@ +name: fork-kustomize-controller +id: ari:cloud:compass:83673fa7-fd28-4f4a-9738-f584064570a7:component/db43f86d-85fe-42e1-954d-457f5a4082b8/83490c6a-9e1a-4db2-88d1-4959e9348685 +description: The GitOps Toolkit Kustomize reconciler +configVersion: 1 +typeId: SERVICE +fields: + tier: 4 +links: + - name: null + type: REPOSITORY + url: https://github.com/verygood-ops/fork-kustomize-controller +relationships: + DEPENDS_ON: [] +labels: + - source:github +customFields: + - name: Logging + type: text + value: null + - name: Metrics + type: text + value: null + - name: Service Name + type: text + value: null + - name: Tracing + type: text + value: null + - name: coverage-metricSourceId + type: text + value: null diff --git a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml index 13a3cc30a..21e9a0de0 100644 --- a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml +++ b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.16.1 name: kustomizations.kustomize.toolkit.fluxcd.io spec: group: kustomize.toolkit.fluxcd.io @@ -17,6 +16,591 @@ spec: singular: kustomization scope: Namespaced versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1 + schema: + openAPIV3Schema: + description: Kustomization is the Schema for the kustomizations API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + KustomizationSpec defines the configuration to calculate the desired state + from a Source using Kustomize. + properties: + commonMetadata: + description: |- + CommonMetadata specifies the common labels and annotations that are + applied to all resources. Any existing label or annotation will be + overridden if its key matches a common one. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the object's metadata. + type: object + labels: + additionalProperties: + type: string + description: Labels to be added to the object's metadata. + type: object + type: object + components: + description: Components specifies relative paths to specifications + of other Components. + items: + type: string + type: array + decryption: + description: Decrypt Kubernetes secrets before applying them on the + cluster. + properties: + provider: + description: Provider is the name of the decryption engine. + enum: + - sops + type: string + secretRef: + description: |- + The secret name containing the private OpenPGP keys used for decryption. + A static credential for a cloud provider defined inside the Secret + takes priority to secret-less authentication with the ServiceAccountName + field. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the service account used to + authenticate with KMS services from cloud providers. If a + static credential for a given cloud provider is defined + inside the Secret referenced by SecretRef, that static + credential takes priority. + type: string + required: + - provider + type: object + deletionPolicy: + description: |- + DeletionPolicy can be used to control garbage collection when this + Kustomization is deleted. Valid values are ('MirrorPrune', 'Delete', + 'WaitForTermination', 'Orphan'). 'MirrorPrune' mirrors the Prune field + (orphan if false, delete if true). Defaults to 'MirrorPrune'. + enum: + - MirrorPrune + - Delete + - WaitForTermination + - Orphan + type: string + dependsOn: + description: |- + DependsOn may contain a meta.NamespacedObjectReference slice + with references to Kustomization resources that must be ready before this + Kustomization can be reconciled. + items: + description: |- + NamespacedObjectReference contains enough information to locate the referenced Kubernetes resource object in any + namespace. + properties: + name: + description: Name of the referent. + type: string + namespace: + description: Namespace of the referent, when not specified it + acts as LocalObjectReference. + type: string + required: + - name + type: object + type: array + force: + default: false + description: |- + Force instructs the controller to recreate resources + when patching fails due to an immutable field change. + type: boolean + healthCheckExprs: + description: |- + HealthCheckExprs is a list of healthcheck expressions for evaluating the + health of custom resources using Common Expression Language (CEL). + The expressions are evaluated only when Wait or HealthChecks are specified. + items: + description: CustomHealthCheck defines the health check for custom + resources. + properties: + apiVersion: + description: APIVersion of the custom resource under evaluation. + type: string + current: + description: |- + Current is the CEL expression that determines if the status + of the custom resource has reached the desired state. + type: string + failed: + description: |- + Failed is the CEL expression that determines if the status + of the custom resource has failed to reach the desired state. + type: string + inProgress: + description: |- + InProgress is the CEL expression that determines if the status + of the custom resource has not yet reached the desired state. + type: string + kind: + description: Kind of the custom resource under evaluation. + type: string + required: + - apiVersion + - current + - kind + type: object + type: array + healthChecks: + description: A list of resources to be included in the health assessment. + items: + description: |- + NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object + in any namespace. + properties: + apiVersion: + description: API version of the referent, if not specified the + Kubernetes preferred version will be used. + type: string + kind: + description: Kind of the referent. + type: string + name: + description: Name of the referent. + type: string + namespace: + description: Namespace of the referent, when not specified it + acts as LocalObjectReference. + type: string + required: + - kind + - name + type: object + type: array + images: + description: |- + Images is a list of (image name, new name, new tag or digest) + for changing image names, tags or digests. This can also be achieved with a + patch, but this operator is simpler to specify. + items: + description: Image contains an image name, a new name, a new tag + or digest, which will replace the original name and tag. + properties: + digest: + description: |- + Digest is the value used to replace the original image tag. + If digest is present NewTag value is ignored. + type: string + name: + description: Name is a tag-less image name. + type: string + newName: + description: NewName is the value used to replace the original + name. + type: string + newTag: + description: NewTag is the value used to replace the original + tag. + type: string + required: + - name + type: object + type: array + interval: + description: |- + The interval at which to reconcile the Kustomization. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + kubeConfig: + description: |- + The KubeConfig for reconciling the Kustomization on a remote cluster. + When used in combination with KustomizationSpec.ServiceAccountName, + forces the controller to act on behalf of that Service Account at the + target cluster. + If the --default-service-account flag is set, its value will be used as + a controller level fallback for when KustomizationSpec.ServiceAccountName + is empty. + properties: + secretRef: + description: |- + SecretRef holds the name of a secret that contains a key with + the kubeconfig file as the value. If no key is set, the key will default + to 'value'. + It is recommended that the kubeconfig is self-contained, and the secret + is regularly updated if credentials such as a cloud-access-token expire. + Cloud specific `cmd-path` auth helpers will not function without adding + binaries and credentials to the Pod that is responsible for reconciling + Kubernetes resources. + properties: + key: + description: Key in the Secret, when not specified an implementation-specific + default key is used. + type: string + name: + description: Name of the Secret. + type: string + required: + - name + type: object + required: + - secretRef + type: object + namePrefix: + description: NamePrefix will prefix the names of all managed resources. + maxLength: 200 + minLength: 1 + type: string + nameSuffix: + description: NameSuffix will suffix the names of all managed resources. + maxLength: 200 + minLength: 1 + type: string + patches: + description: |- + Strategic merge and JSON patches, defined as inline YAML objects, + capable of targeting objects based on kind, label and annotation selectors. + items: + description: |- + Patch contains an inline StrategicMerge or JSON6902 patch, and the target the patch should + be applied to. + properties: + patch: + description: |- + Patch contains an inline StrategicMerge patch or an inline JSON6902 patch with + an array of operation objects. + type: string + target: + description: Target points to the resources that the patch document + should be applied to. + properties: + annotationSelector: + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource annotations. + type: string + group: + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + kind: + description: |- + Kind of the API Group to select resources from. + Together with Group and Version it is capable of unambiguously + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource labels. + type: string + name: + description: Name to match resources with. + type: string + namespace: + description: Namespace to select resources from. + type: string + version: + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + type: object + required: + - patch + type: object + type: array + path: + description: |- + Path to the directory containing the kustomization.yaml file, or the + set of plain YAMLs a kustomization.yaml should be generated for. + Defaults to 'None', which translates to the root path of the SourceRef. + type: string + postBuild: + description: |- + PostBuild describes which actions to perform on the YAML manifest + generated by building the kustomize overlay. + properties: + substitute: + additionalProperties: + type: string + description: |- + Substitute holds a map of key/value pairs. + The variables defined in your YAML manifests that match any of the keys + defined in the map will be substituted with the set value. + Includes support for bash string replacement functions + e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}. + type: object + substituteFrom: + description: |- + SubstituteFrom holds references to ConfigMaps and Secrets containing + the variables and their values to be substituted in the YAML manifests. + The ConfigMap and the Secret data keys represent the var names, and they + must match the vars declared in the manifests for the substitution to + happen. + items: + description: |- + SubstituteReference contains a reference to a resource containing + the variables name and value. + properties: + kind: + description: Kind of the values referent, valid values are + ('Secret', 'ConfigMap'). + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name of the values referent. Should reside in the same namespace as the + referring resource. + maxLength: 253 + minLength: 1 + type: string + optional: + default: false + description: |- + Optional indicates whether the referenced resource must exist, or whether to + tolerate its absence. If true and the referenced resource is absent, proceed + as if the resource was present but empty, without any variables defined. + type: boolean + required: + - kind + - name + type: object + type: array + type: object + prune: + description: Prune enables garbage collection. + type: boolean + retryInterval: + description: |- + The interval at which to retry a previously failed reconciliation. + When not specified, the controller uses the KustomizationSpec.Interval + value to retry failures. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + serviceAccountName: + description: |- + The name of the Kubernetes service account to impersonate + when reconciling this Kustomization. + type: string + sourceRef: + description: Reference of the source where the kustomization file + is. + properties: + apiVersion: + description: API version of the referent. + type: string + kind: + description: Kind of the referent. + enum: + - OCIRepository + - GitRepository + - Bucket + type: string + name: + description: Name of the referent. + type: string + namespace: + description: |- + Namespace of the referent, defaults to the namespace of the Kubernetes + resource object that contains the reference. + type: string + required: + - kind + - name + type: object + suspend: + description: |- + This flag tells the controller to suspend subsequent kustomize executions, + it does not apply to already started executions. Defaults to false. + type: boolean + targetNamespace: + description: |- + TargetNamespace sets or overrides the namespace in the + kustomization.yaml file. + maxLength: 63 + minLength: 1 + type: string + timeout: + description: |- + Timeout for validation, apply and health checking operations. + Defaults to 'Interval' duration. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + wait: + description: |- + Wait instructs the controller to check the health of all the reconciled + resources. When enabled, the HealthChecks are ignored. Defaults to false. + type: boolean + required: + - interval + - prune + - sourceRef + type: object + status: + default: + observedGeneration: -1 + description: KustomizationStatus defines the observed state of a kustomization. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + inventory: + description: |- + Inventory contains the list of Kubernetes resource object references that + have been successfully applied. + properties: + entries: + description: Entries of Kubernetes resource object references. + items: + description: ResourceRef contains the information necessary + to locate a resource within a cluster. + properties: + id: + description: |- + ID is the string representation of the Kubernetes resource object's metadata, + in the format '___'. + type: string + v: + description: Version is the API version of the Kubernetes + resource object's kind. + type: string + required: + - id + - v + type: object + type: array + required: + - entries + type: object + lastAppliedOriginRevision: + description: |- + The last successfully applied origin revision. + Equals the origin revision of the applied Artifact from the referenced Source. + Usually present on the Metadata of the applied Artifact and depends on the + Source type, e.g. for OCI it's the value associated with the key + "org.opencontainers.image.revision". + type: string + lastAppliedRevision: + description: |- + The last successfully applied revision. + Equals the Revision of the applied Artifact from the referenced Source. + type: string + lastAttemptedRevision: + description: LastAttemptedRevision is the revision of the last reconciliation + attempt. + type: string + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last reconciled generation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} - additionalPrinterColumns: - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready @@ -27,20 +611,27 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + deprecated: true + deprecationWarning: v1beta1 Kustomization is deprecated, upgrade to v1 name: v1beta1 schema: openAPIV3Schema: description: Kustomization is the Schema for the kustomizations API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -70,12 +661,14 @@ spec: - provider type: object dependsOn: - description: DependsOn may contain a meta.NamespacedObjectReference - slice with references to Kustomization resources that must be ready - before this Kustomization can be reconciled. + description: |- + DependsOn may contain a meta.NamespacedObjectReference slice + with references to Kustomization resources that must be ready before this + Kustomization can be reconciled. items: - description: NamespacedObjectReference contains enough information - to locate the referenced Kubernetes resource object in any namespace. + description: |- + NamespacedObjectReference contains enough information to locate the referenced Kubernetes resource object in any + namespace. properties: name: description: Name of the referent. @@ -90,15 +683,16 @@ spec: type: array force: default: false - description: Force instructs the controller to recreate resources + description: |- + Force instructs the controller to recreate resources when patching fails due to an immutable field change. type: boolean healthChecks: description: A list of resources to be included in the health assessment. items: - description: NamespacedObjectKindReference contains enough information - to locate the typed referenced Kubernetes resource object in any - namespace. + description: |- + NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object + in any namespace. properties: apiVersion: description: API version of the referent, if not specified the @@ -120,16 +714,18 @@ spec: type: object type: array images: - description: Images is a list of (image name, new name, new tag or - digest) for changing image names, tags or digests. This can also - be achieved with a patch, but this operator is simpler to specify. + description: |- + Images is a list of (image name, new name, new tag or digest) + for changing image names, tags or digests. This can also be achieved with a + patch, but this operator is simpler to specify. items: description: Image contains an image name, a new name, a new tag or digest, which will replace the original name and tag. properties: digest: - description: Digest is the value used to replace the original - image tag. If digest is present NewTag value is ignored. + description: |- + Digest is the value used to replace the original image tag. + If digest is present NewTag value is ignored. type: string name: description: Name is a tag-less image name. @@ -150,19 +746,20 @@ spec: description: The interval at which to reconcile the Kustomization. type: string kubeConfig: - description: The KubeConfig for reconciling the Kustomization on a - remote cluster. When specified, KubeConfig takes precedence over - ServiceAccountName. + description: |- + The KubeConfig for reconciling the Kustomization on a remote cluster. + When specified, KubeConfig takes precedence over ServiceAccountName. properties: secretRef: - description: SecretRef holds the name to a secret that contains - a 'value' key with the kubeconfig file as the value. It must - be in the same namespace as the Kustomization. It is recommended - that the kubeconfig is self-contained, and the secret is regularly - updated if credentials such as a cloud-access-token expire. - Cloud specific `cmd-path` auth helpers will not function without - adding binaries and credentials to the Pod that is responsible - for reconciling the Kustomization. + description: |- + SecretRef holds the name to a secret that contains a 'value' key with + the kubeconfig file as the value. It must be in the same namespace as + the Kustomization. + It is recommended that the kubeconfig is self-contained, and the secret + is regularly updated if credentials such as a cloud-access-token expire. + Cloud specific `cmd-path` auth helpers will not function without adding + binaries and credentials to the Pod that is responsible for reconciling + the Kustomization. properties: name: description: Name of the referent. @@ -170,42 +767,50 @@ spec: required: - name type: object + required: + - secretRef type: object patches: - description: Strategic merge and JSON patches, defined as inline YAML - objects, capable of targeting objects based on kind, label and annotation - selectors. + description: |- + Strategic merge and JSON patches, defined as inline YAML objects, + capable of targeting objects based on kind, label and annotation selectors. items: - description: Patch contains an inline StrategicMerge or JSON6902 - patch, and the target the patch should be applied to. + description: |- + Patch contains an inline StrategicMerge or JSON6902 patch, and the target the patch should + be applied to. properties: patch: - description: Patch contains an inline StrategicMerge patch or - an inline JSON6902 patch with an array of operation objects. + description: |- + Patch contains an inline StrategicMerge patch or an inline JSON6902 patch with + an array of operation objects. type: string target: description: Target points to the resources that the patch document should be applied to. properties: annotationSelector: - description: AnnotationSelector is a string that follows - the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource annotations. type: string group: - description: Group is the API group to select resources - from. Together with Version and Kind it is capable of - unambiguously identifying and/or selecting resources. + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string kind: - description: Kind of the API Group to select resources from. + description: |- + Kind of the API Group to select resources from. Together with Group and Version it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string labelSelector: - description: LabelSelector is a string that follows the - label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource labels. type: string name: @@ -215,11 +820,14 @@ spec: description: Namespace to select resources from. type: string version: - description: Version of the API Group to select resources - from. Together with Group and Kind it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string type: object + required: + - patch type: object type: array patchesJson6902: @@ -232,18 +840,20 @@ spec: description: Patch contains the JSON6902 patch document with an array of operation objects. items: - description: JSON6902 is a JSON6902 operation object. https://datatracker.ietf.org/doc/html/rfc6902#section-4 + description: |- + JSON6902 is a JSON6902 operation object. + https://datatracker.ietf.org/doc/html/rfc6902#section-4 properties: from: - description: From contains a JSON-pointer value that references - a location within the target document where the operation - is performed. The meaning of the value depends on the - value of Op, and is NOT taken into account by all operations. + description: |- + From contains a JSON-pointer value that references a location within the target document where the operation is + performed. The meaning of the value depends on the value of Op, and is NOT taken into account by all operations. type: string op: - description: Op indicates the operation to perform. Its - value MUST be one of "add", "remove", "replace", "move", - "copy", or "test". https://datatracker.ietf.org/doc/html/rfc6902#section-4 + description: |- + Op indicates the operation to perform. Its value MUST be one of "add", "remove", "replace", "move", "copy", or + "test". + https://datatracker.ietf.org/doc/html/rfc6902#section-4 enum: - test - remove @@ -253,15 +863,14 @@ spec: - copy type: string path: - description: Path contains the JSON-pointer value that - references a location within the target document where - the operation is performed. The meaning of the value - depends on the value of Op. + description: |- + Path contains the JSON-pointer value that references a location within the target document where the operation + is performed. The meaning of the value depends on the value of Op. type: string value: - description: Value contains a valid JSON structure. The - meaning of the value depends on the value of Op, and - is NOT taken into account by all operations. + description: |- + Value contains a valid JSON structure. The meaning of the value depends on the value of Op, and is NOT taken into + account by all operations. x-kubernetes-preserve-unknown-fields: true required: - op @@ -273,24 +882,28 @@ spec: should be applied to. properties: annotationSelector: - description: AnnotationSelector is a string that follows - the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource annotations. type: string group: - description: Group is the API group to select resources - from. Together with Version and Kind it is capable of - unambiguously identifying and/or selecting resources. + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string kind: - description: Kind of the API Group to select resources from. + description: |- + Kind of the API Group to select resources from. Together with Group and Version it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string labelSelector: - description: LabelSelector is a string that follows the - label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource labels. type: string name: @@ -300,9 +913,10 @@ spec: description: Namespace to select resources from. type: string version: - description: Version of the API Group to select resources - from. Together with Group and Kind it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string type: object required: @@ -316,33 +930,37 @@ spec: x-kubernetes-preserve-unknown-fields: true type: array path: - description: Path to the directory containing the kustomization.yaml - file, or the set of plain YAMLs a kustomization.yaml should be generated - for. Defaults to 'None', which translates to the root path of the - SourceRef. + description: |- + Path to the directory containing the kustomization.yaml file, or the + set of plain YAMLs a kustomization.yaml should be generated for. + Defaults to 'None', which translates to the root path of the SourceRef. type: string postBuild: - description: PostBuild describes which actions to perform on the YAML - manifest generated by building the kustomize overlay. + description: |- + PostBuild describes which actions to perform on the YAML manifest + generated by building the kustomize overlay. properties: substitute: additionalProperties: type: string - description: Substitute holds a map of key/value pairs. The variables - defined in your YAML manifests that match any of the keys defined - in the map will be substituted with the set value. Includes - support for bash string replacement functions e.g. ${var:=default}, - ${var:position} and ${var/substring/replacement}. + description: |- + Substitute holds a map of key/value pairs. + The variables defined in your YAML manifests + that match any of the keys defined in the map + will be substituted with the set value. + Includes support for bash string replacement functions + e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}. type: object substituteFrom: - description: SubstituteFrom holds references to ConfigMaps and - Secrets containing the variables and their values to be substituted - in the YAML manifests. The ConfigMap and the Secret data keys - represent the var names and they must match the vars declared - in the manifests for the substitution to happen. + description: |- + SubstituteFrom holds references to ConfigMaps and Secrets containing + the variables and their values to be substituted in the YAML manifests. + The ConfigMap and the Secret data keys represent the var names and they + must match the vars declared in the manifests for the substitution to happen. items: - description: SubstituteReference contains a reference to a resource - containing the variables name and value. + description: |- + SubstituteReference contains a reference to a resource containing + the variables name and value. properties: kind: description: Kind of the values referent, valid values are @@ -352,8 +970,9 @@ spec: - ConfigMap type: string name: - description: Name of the values referent. Should reside - in the same namespace as the referring resource. + description: |- + Name of the values referent. Should reside in the same namespace as the + referring resource. maxLength: 253 minLength: 1 type: string @@ -367,12 +986,14 @@ spec: description: Prune enables garbage collection. type: boolean retryInterval: - description: The interval at which to retry a previously failed reconciliation. + description: |- + The interval at which to retry a previously failed reconciliation. When not specified, the controller uses the KustomizationSpec.Interval value to retry failures. type: string serviceAccountName: - description: The name of the Kubernetes service account to impersonate + description: |- + The name of the Kubernetes service account to impersonate when reconciling this Kustomization. type: string sourceRef: @@ -400,26 +1021,29 @@ spec: - name type: object suspend: - description: This flag tells the controller to suspend subsequent - kustomize executions, it does not apply to already started executions. - Defaults to false. + description: |- + This flag tells the controller to suspend subsequent kustomize executions, + it does not apply to already started executions. Defaults to false. type: boolean targetNamespace: - description: TargetNamespace sets or overrides the namespace in the + description: |- + TargetNamespace sets or overrides the namespace in the kustomization.yaml file. maxLength: 63 minLength: 1 type: string timeout: - description: Timeout for validation, apply and health checking operations. + description: |- + Timeout for validation, apply and health checking operations. Defaults to 'Interval' duration. type: string validation: - description: Validate the Kubernetes objects before applying them - on the cluster. The validation strategy can be 'client' (local dry-run), - 'server' (APIServer dry-run) or 'none'. When 'Force' is 'true', - validation will fallback to 'client' if set to 'server' because - server-side validation is not supported in this scenario. + description: |- + Validate the Kubernetes objects before applying them on the cluster. + The validation strategy can be 'client' (local dry-run), 'server' + (APIServer dry-run) or 'none'. + When 'Force' is 'true', validation will fallback to 'client' if set to + 'server' because server-side validation is not supported in this scenario. enum: - none - client @@ -437,43 +1061,35 @@ spec: properties: conditions: items: - description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. maxLength: 32768 type: string observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 @@ -488,10 +1104,6 @@ spec: type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -504,17 +1116,19 @@ spec: type: object type: array lastAppliedRevision: - description: The last successfully applied revision. The revision - format for Git sources is /. + description: |- + The last successfully applied revision. + The revision format for Git sources is /. type: string lastAttemptedRevision: description: LastAttemptedRevision is the revision of the last reconciliation attempt. type: string lastHandledReconcileAt: - description: LastHandledReconcileAt holds the value of the most recent - reconcile request value, so a change of the annotation value can - be detected. + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. type: string observedGeneration: description: ObservedGeneration is the last reconciled generation. @@ -529,8 +1143,9 @@ spec: entries: description: A list of Kubernetes kinds grouped by namespace. items: - description: Snapshot holds the metadata of namespaced Kubernetes - objects + description: |- + Snapshot holds the metadata of namespaced + Kubernetes objects properties: kinds: additionalProperties: @@ -564,20 +1179,27 @@ spec: - jsonPath: .status.conditions[?(@.type=="Ready")].message name: Status type: string + deprecated: true + deprecationWarning: v1beta2 Kustomization is deprecated, upgrade to v1 name: v1beta2 schema: openAPIV3Schema: description: Kustomization is the Schema for the kustomizations API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -585,9 +1207,25 @@ spec: description: KustomizationSpec defines the configuration to calculate the desired state from a Source using Kustomize. properties: + commonMetadata: + description: |- + CommonMetadata specifies the common labels and annotations that are applied to all resources. + Any existing label or annotation will be overridden if its key matches a common one. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the object's metadata. + type: object + labels: + additionalProperties: + type: string + description: Labels to be added to the object's metadata. + type: object + type: object components: description: Components specifies relative paths to specifications - of other Components + of other Components. items: type: string type: array @@ -614,12 +1252,14 @@ spec: - provider type: object dependsOn: - description: DependsOn may contain a meta.NamespacedObjectReference - slice with references to Kustomization resources that must be ready - before this Kustomization can be reconciled. + description: |- + DependsOn may contain a meta.NamespacedObjectReference slice + with references to Kustomization resources that must be ready before this + Kustomization can be reconciled. items: - description: NamespacedObjectReference contains enough information - to locate the referenced Kubernetes resource object in any namespace. + description: |- + NamespacedObjectReference contains enough information to locate the referenced Kubernetes resource object in any + namespace. properties: name: description: Name of the referent. @@ -634,15 +1274,16 @@ spec: type: array force: default: false - description: Force instructs the controller to recreate resources + description: |- + Force instructs the controller to recreate resources when patching fails due to an immutable field change. type: boolean healthChecks: description: A list of resources to be included in the health assessment. items: - description: NamespacedObjectKindReference contains enough information - to locate the typed referenced Kubernetes resource object in any - namespace. + description: |- + NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object + in any namespace. properties: apiVersion: description: API version of the referent, if not specified the @@ -664,16 +1305,18 @@ spec: type: object type: array images: - description: Images is a list of (image name, new name, new tag or - digest) for changing image names, tags or digests. This can also - be achieved with a patch, but this operator is simpler to specify. + description: |- + Images is a list of (image name, new name, new tag or digest) + for changing image names, tags or digests. This can also be achieved with a + patch, but this operator is simpler to specify. items: description: Image contains an image name, a new name, a new tag or digest, which will replace the original name and tag. properties: digest: - description: Digest is the value used to replace the original - image tag. If digest is present NewTag value is ignored. + description: |- + Digest is the value used to replace the original image tag. + If digest is present NewTag value is ignored. type: string name: description: Name is a tag-less image name. @@ -695,21 +1338,24 @@ spec: pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ type: string kubeConfig: - description: The KubeConfig for reconciling the Kustomization on a - remote cluster. When used in combination with KustomizationSpec.ServiceAccountName, - forces the controller to act on behalf of that Service Account at - the target cluster. If the --default-service-account flag is set, - its value will be used as a controller level fallback for when KustomizationSpec.ServiceAccountName + description: |- + The KubeConfig for reconciling the Kustomization on a remote cluster. + When used in combination with KustomizationSpec.ServiceAccountName, + forces the controller to act on behalf of that Service Account at the + target cluster. + If the --default-service-account flag is set, its value will be used as + a controller level fallback for when KustomizationSpec.ServiceAccountName is empty. properties: secretRef: - description: SecretRef holds the name of a secret that contains - a key with the kubeconfig file as the value. If no key is set, - the key will default to 'value'. It is recommended that the - kubeconfig is self-contained, and the secret is regularly updated - if credentials such as a cloud-access-token expire. Cloud specific - `cmd-path` auth helpers will not function without adding binaries - and credentials to the Pod that is responsible for reconciling + description: |- + SecretRef holds the name of a secret that contains a key with + the kubeconfig file as the value. If no key is set, the key will default + to 'value'. + It is recommended that the kubeconfig is self-contained, and the secret + is regularly updated if credentials such as a cloud-access-token expire. + Cloud specific `cmd-path` auth helpers will not function without adding + binaries and credentials to the Pod that is responsible for reconciling Kubernetes resources. properties: key: @@ -726,40 +1372,46 @@ spec: - secretRef type: object patches: - description: Strategic merge and JSON patches, defined as inline YAML - objects, capable of targeting objects based on kind, label and annotation - selectors. + description: |- + Strategic merge and JSON patches, defined as inline YAML objects, + capable of targeting objects based on kind, label and annotation selectors. items: - description: Patch contains an inline StrategicMerge or JSON6902 - patch, and the target the patch should be applied to. + description: |- + Patch contains an inline StrategicMerge or JSON6902 patch, and the target the patch should + be applied to. properties: patch: - description: Patch contains an inline StrategicMerge patch or - an inline JSON6902 patch with an array of operation objects. + description: |- + Patch contains an inline StrategicMerge patch or an inline JSON6902 patch with + an array of operation objects. type: string target: description: Target points to the resources that the patch document should be applied to. properties: annotationSelector: - description: AnnotationSelector is a string that follows - the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource annotations. type: string group: - description: Group is the API group to select resources - from. Together with Version and Kind it is capable of - unambiguously identifying and/or selecting resources. + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string kind: - description: Kind of the API Group to select resources from. + description: |- + Kind of the API Group to select resources from. Together with Group and Version it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string labelSelector: - description: LabelSelector is a string that follows the - label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource labels. type: string name: @@ -769,16 +1421,20 @@ spec: description: Namespace to select resources from. type: string version: - description: Version of the API Group to select resources - from. Together with Group and Kind it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string type: object + required: + - patch type: object type: array patchesJson6902: - description: 'JSON 6902 patches, defined as inline YAML objects. Deprecated: - Use Patches instead.' + description: |- + JSON 6902 patches, defined as inline YAML objects. + Deprecated: Use Patches instead. items: description: JSON6902Patch contains a JSON6902 patch and the target the patch should be applied to. @@ -787,18 +1443,20 @@ spec: description: Patch contains the JSON6902 patch document with an array of operation objects. items: - description: JSON6902 is a JSON6902 operation object. https://datatracker.ietf.org/doc/html/rfc6902#section-4 + description: |- + JSON6902 is a JSON6902 operation object. + https://datatracker.ietf.org/doc/html/rfc6902#section-4 properties: from: - description: From contains a JSON-pointer value that references - a location within the target document where the operation - is performed. The meaning of the value depends on the - value of Op, and is NOT taken into account by all operations. + description: |- + From contains a JSON-pointer value that references a location within the target document where the operation is + performed. The meaning of the value depends on the value of Op, and is NOT taken into account by all operations. type: string op: - description: Op indicates the operation to perform. Its - value MUST be one of "add", "remove", "replace", "move", - "copy", or "test". https://datatracker.ietf.org/doc/html/rfc6902#section-4 + description: |- + Op indicates the operation to perform. Its value MUST be one of "add", "remove", "replace", "move", "copy", or + "test". + https://datatracker.ietf.org/doc/html/rfc6902#section-4 enum: - test - remove @@ -808,15 +1466,14 @@ spec: - copy type: string path: - description: Path contains the JSON-pointer value that - references a location within the target document where - the operation is performed. The meaning of the value - depends on the value of Op. + description: |- + Path contains the JSON-pointer value that references a location within the target document where the operation + is performed. The meaning of the value depends on the value of Op. type: string value: - description: Value contains a valid JSON structure. The - meaning of the value depends on the value of Op, and - is NOT taken into account by all operations. + description: |- + Value contains a valid JSON structure. The meaning of the value depends on the value of Op, and is NOT taken into + account by all operations. x-kubernetes-preserve-unknown-fields: true required: - op @@ -828,24 +1485,28 @@ spec: should be applied to. properties: annotationSelector: - description: AnnotationSelector is a string that follows - the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource annotations. type: string group: - description: Group is the API group to select resources - from. Together with Version and Kind it is capable of - unambiguously identifying and/or selecting resources. + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string kind: - description: Kind of the API Group to select resources from. + description: |- + Kind of the API Group to select resources from. Together with Group and Version it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string labelSelector: - description: LabelSelector is a string that follows the - label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api It matches with the resource labels. type: string name: @@ -855,9 +1516,10 @@ spec: description: Namespace to select resources from. type: string version: - description: Version of the API Group to select resources - from. Together with Group and Kind it is capable of unambiguously - identifying and/or selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md type: string type: object required: @@ -866,39 +1528,44 @@ spec: type: object type: array patchesStrategicMerge: - description: 'Strategic merge patches, defined as inline YAML objects. - Deprecated: Use Patches instead.' + description: |- + Strategic merge patches, defined as inline YAML objects. + Deprecated: Use Patches instead. items: x-kubernetes-preserve-unknown-fields: true type: array path: - description: Path to the directory containing the kustomization.yaml - file, or the set of plain YAMLs a kustomization.yaml should be generated - for. Defaults to 'None', which translates to the root path of the - SourceRef. + description: |- + Path to the directory containing the kustomization.yaml file, or the + set of plain YAMLs a kustomization.yaml should be generated for. + Defaults to 'None', which translates to the root path of the SourceRef. type: string postBuild: - description: PostBuild describes which actions to perform on the YAML - manifest generated by building the kustomize overlay. + description: |- + PostBuild describes which actions to perform on the YAML manifest + generated by building the kustomize overlay. properties: substitute: additionalProperties: type: string - description: Substitute holds a map of key/value pairs. The variables - defined in your YAML manifests that match any of the keys defined - in the map will be substituted with the set value. Includes - support for bash string replacement functions e.g. ${var:=default}, - ${var:position} and ${var/substring/replacement}. + description: |- + Substitute holds a map of key/value pairs. + The variables defined in your YAML manifests + that match any of the keys defined in the map + will be substituted with the set value. + Includes support for bash string replacement functions + e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}. type: object substituteFrom: - description: SubstituteFrom holds references to ConfigMaps and - Secrets containing the variables and their values to be substituted - in the YAML manifests. The ConfigMap and the Secret data keys - represent the var names and they must match the vars declared - in the manifests for the substitution to happen. + description: |- + SubstituteFrom holds references to ConfigMaps and Secrets containing + the variables and their values to be substituted in the YAML manifests. + The ConfigMap and the Secret data keys represent the var names and they + must match the vars declared in the manifests for the substitution to happen. items: - description: SubstituteReference contains a reference to a resource - containing the variables name and value. + description: |- + SubstituteReference contains a reference to a resource containing + the variables name and value. properties: kind: description: Kind of the values referent, valid values are @@ -908,18 +1575,18 @@ spec: - ConfigMap type: string name: - description: Name of the values referent. Should reside - in the same namespace as the referring resource. + description: |- + Name of the values referent. Should reside in the same namespace as the + referring resource. maxLength: 253 minLength: 1 type: string optional: default: false - description: Optional indicates whether the referenced resource - must exist, or whether to tolerate its absence. If true - and the referenced resource is absent, proceed as if the - resource was present but empty, without any variables - defined. + description: |- + Optional indicates whether the referenced resource must exist, or whether to + tolerate its absence. If true and the referenced resource is absent, proceed + as if the resource was present but empty, without any variables defined. type: boolean required: - kind @@ -931,13 +1598,15 @@ spec: description: Prune enables garbage collection. type: boolean retryInterval: - description: The interval at which to retry a previously failed reconciliation. + description: |- + The interval at which to retry a previously failed reconciliation. When not specified, the controller uses the KustomizationSpec.Interval value to retry failures. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ type: string serviceAccountName: - description: The name of the Kubernetes service account to impersonate + description: |- + The name of the Kubernetes service account to impersonate when reconciling this Kustomization. type: string sourceRef: @@ -966,18 +1635,20 @@ spec: - name type: object suspend: - description: This flag tells the controller to suspend subsequent - kustomize executions, it does not apply to already started executions. - Defaults to false. + description: |- + This flag tells the controller to suspend subsequent kustomize executions, + it does not apply to already started executions. Defaults to false. type: boolean targetNamespace: - description: TargetNamespace sets or overrides the namespace in the + description: |- + TargetNamespace sets or overrides the namespace in the kustomization.yaml file. maxLength: 63 minLength: 1 type: string timeout: - description: Timeout for validation, apply and health checking operations. + description: |- + Timeout for validation, apply and health checking operations. Defaults to 'Interval' duration. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ type: string @@ -989,9 +1660,9 @@ spec: - server type: string wait: - description: Wait instructs the controller to check the health of - all the reconciled resources. When enabled, the HealthChecks are - ignored. Defaults to false. + description: |- + Wait instructs the controller to check the health of all the reconciled resources. + When enabled, the HealthChecks are ignored. Defaults to false. type: boolean required: - interval @@ -1005,43 +1676,35 @@ spec: properties: conditions: items: - description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. maxLength: 32768 type: string observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 @@ -1056,10 +1719,6 @@ spec: type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -1082,8 +1741,9 @@ spec: to locate a resource within a cluster. properties: id: - description: ID is the string representation of the Kubernetes - resource object's metadata, in the format '___'. + description: |- + ID is the string representation of the Kubernetes resource object's metadata, + in the format '___'. type: string v: description: Version is the API version of the Kubernetes @@ -1098,17 +1758,19 @@ spec: - entries type: object lastAppliedRevision: - description: The last successfully applied revision. The revision - format for Git sources is /. + description: |- + The last successfully applied revision. + Equals the Revision of the applied Artifact from the referenced Source. type: string lastAttemptedRevision: description: LastAttemptedRevision is the revision of the last reconciliation attempt. type: string lastHandledReconcileAt: - description: LastHandledReconcileAt holds the value of the most recent - reconcile request value, so a change of the annotation value can - be detected. + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. type: string observedGeneration: description: ObservedGeneration is the last reconciled generation. @@ -1117,12 +1779,6 @@ spec: type: object type: object served: true - storage: true + storage: false subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 871b584ab..68f9bfba6 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -2,8 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: kustomize-system resources: -- https://github.com/fluxcd/source-controller/releases/download/v0.34.0/source-controller.crds.yaml -- https://github.com/fluxcd/source-controller/releases/download/v0.34.0/source-controller.deployment.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.6.0/source-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.6.0/source-controller.deployment.yaml - ../crd - ../rbac - ../manager diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index d5378eb99..0295a87d2 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ resources: images: - name: fluxcd/kustomize-controller newName: fluxcd/kustomize-controller - newTag: v0.33.0 + newTag: v1.6.0 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index eb743029a..2faa30768 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -2,7 +2,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: manager-role rules: - apiGroups: @@ -22,6 +21,12 @@ rules: verbs: - create - patch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create - apiGroups: - kustomize.toolkit.fluxcd.io resources: diff --git a/config/samples/kustomize_v1beta1_kustomization.yaml b/config/samples/kustomize_v1_kustomization.yaml similarity index 85% rename from config/samples/kustomize_v1beta1_kustomization.yaml rename to config/samples/kustomize_v1_kustomization.yaml index 35164984e..d296984bd 100644 --- a/config/samples/kustomize_v1beta1_kustomization.yaml +++ b/config/samples/kustomize_v1_kustomization.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: webapp-dev @@ -12,7 +12,7 @@ spec: wait: true timeout: 2m --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: webapp-production diff --git a/config/samples/source_v1beta1_gitrepository.yaml b/config/samples/source_v1_gitrepository.yaml similarity index 76% rename from config/samples/source_v1beta1_gitrepository.yaml rename to config/samples/source_v1_gitrepository.yaml index 60b5a4d4b..431f83821 100644 --- a/config/samples/source_v1beta1_gitrepository.yaml +++ b/config/samples/source_v1_gitrepository.yaml @@ -1,4 +1,4 @@ -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: webapp-latest @@ -8,7 +8,7 @@ spec: ref: branch: master --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: webapp-releases diff --git a/config/testdata/crds-crs/cert-manager.yaml b/config/testdata/crds-crs/cert-manager.yaml index 1cec3dc54..0f43c6f77 100644 --- a/config/testdata/crds-crs/cert-manager.yaml +++ b/config/testdata/crds-crs/cert-manager.yaml @@ -1,4 +1,4 @@ -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: certs @@ -8,7 +8,7 @@ spec: ref: tag: "v1.1.0" --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: certs diff --git a/config/testdata/dependencies/backend.yaml b/config/testdata/dependencies/backend.yaml index 15ceba5e2..820cb29b1 100644 --- a/config/testdata/dependencies/backend.yaml +++ b/config/testdata/dependencies/backend.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: backend @@ -11,9 +11,8 @@ spec: sourceRef: kind: GitRepository name: webapp - validation: server healthChecks: - kind: Deployment name: backend namespace: webapp - timeout: 2m \ No newline at end of file + timeout: 2m diff --git a/config/testdata/dependencies/common.yaml b/config/testdata/dependencies/common.yaml index 5c5ac2493..f605aa8c7 100644 --- a/config/testdata/dependencies/common.yaml +++ b/config/testdata/dependencies/common.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: common @@ -9,4 +9,3 @@ spec: sourceRef: kind: GitRepository name: webapp - validation: client diff --git a/config/testdata/dependencies/frontend.yaml b/config/testdata/dependencies/frontend.yaml index b26f67091..38d0bfeb1 100644 --- a/config/testdata/dependencies/frontend.yaml +++ b/config/testdata/dependencies/frontend.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: frontend @@ -12,9 +12,8 @@ spec: sourceRef: kind: GitRepository name: webapp - validation: server healthChecks: - kind: Deployment name: frontend namespace: webapp - timeout: 2m \ No newline at end of file + timeout: 2m diff --git a/config/testdata/dependencies/source.yaml b/config/testdata/dependencies/source.yaml index b6dd9a630..5d1e3a543 100644 --- a/config/testdata/dependencies/source.yaml +++ b/config/testdata/dependencies/source.yaml @@ -1,4 +1,4 @@ -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: webapp @@ -6,4 +6,4 @@ spec: interval: 10m url: https://github.com/stefanprodan/podinfo ref: - semver: ">=3.2.3" + semver: ">=6.3.5" diff --git a/config/testdata/impersonation/test.yaml b/config/testdata/impersonation/test.yaml index 0aa960298..3c3c64651 100644 --- a/config/testdata/impersonation/test.yaml +++ b/config/testdata/impersonation/test.yaml @@ -33,7 +33,7 @@ subjects: name: gotk-reconciler namespace: impersonation --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo @@ -42,9 +42,9 @@ spec: interval: 5m url: https://github.com/stefanprodan/podinfo ref: - tag: "6.3.0" + tag: "6.3.5" --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo diff --git a/config/testdata/managed-fields/podinfo.yaml b/config/testdata/managed-fields/podinfo.yaml index 1f065fc08..7e3c5a8c5 100644 --- a/config/testdata/managed-fields/podinfo.yaml +++ b/config/testdata/managed-fields/podinfo.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo @@ -12,7 +12,7 @@ spec: timeout: 1m targetNamespace: managed-fields --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo @@ -20,4 +20,4 @@ spec: interval: 5m url: https://github.com/stefanprodan/podinfo ref: - semver: "6.0.0" + semver: "6.3.5" diff --git a/config/testdata/oci/podinfo.yaml b/config/testdata/oci/podinfo.yaml index fc80f79f5..152fe5b43 100644 --- a/config/testdata/oci/podinfo.yaml +++ b/config/testdata/oci/podinfo.yaml @@ -1,4 +1,4 @@ -apiVersion: source.toolkit.fluxcd.io/v1beta2 +apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: oci @@ -7,9 +7,9 @@ spec: interval: 10m url: oci://ghcr.io/stefanprodan/manifests/podinfo ref: - tag: "6.3.0" + tag: "6.3.5" --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: oci @@ -17,7 +17,7 @@ metadata: spec: targetNamespace: oci interval: 10m - path: "./kustomize" + path: "./" prune: true sourceRef: kind: OCIRepository diff --git a/config/testdata/overlays/production.yaml b/config/testdata/overlays/production.yaml index 96381c72f..d051cd763 100644 --- a/config/testdata/overlays/production.yaml +++ b/config/testdata/overlays/production.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: webapp-production @@ -9,7 +9,6 @@ spec: sourceRef: kind: GitRepository name: webapp-releases - validation: client healthChecks: - kind: Deployment name: backend @@ -19,7 +18,7 @@ spec: namespace: production timeout: 2m --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: webapp-releases @@ -27,4 +26,4 @@ spec: interval: 5m url: https://github.com/stefanprodan/podinfo ref: - semver: ">=3.2.3" + semver: ">=6.3.5" diff --git a/config/testdata/overlays/staging.yaml b/config/testdata/overlays/staging.yaml index 81dc44e28..70aa176c2 100644 --- a/config/testdata/overlays/staging.yaml +++ b/config/testdata/overlays/staging.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: webapp-staging @@ -9,7 +9,6 @@ spec: sourceRef: kind: GitRepository name: webapp-releases - validation: client healthChecks: - kind: Deployment name: backend @@ -19,7 +18,7 @@ spec: namespace: staging timeout: 2m --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: webapp-latest diff --git a/config/testdata/status-defaults/empty-kustomization.yaml b/config/testdata/status-defaults/empty-kustomization.yaml index 80620f12b..d4fd91975 100644 --- a/config/testdata/status-defaults/empty-kustomization.yaml +++ b/config/testdata/status-defaults/empty-kustomization.yaml @@ -1,4 +1,4 @@ -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: status-defaults diff --git a/controllers/testdata/sops/.sops.yaml b/controllers/testdata/sops/.sops.yaml deleted file mode 100644 index 04517a65d..000000000 --- a/controllers/testdata/sops/.sops.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# creation rules are evaluated sequentially, the first match wins -creation_rules: - # files using age - - path_regex: \.age.yaml$ - encrypted_regex: ^(data|stringData)$ - age: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 - - path_regex: month.yaml$ - pgp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1 - # fallback to PGP - - encrypted_regex: ^(data|stringData)$ - pgp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1 diff --git a/controllers/testdata/sops/day.txt b/controllers/testdata/sops/day.txt deleted file mode 100644 index 9c7fc6a64..000000000 --- a/controllers/testdata/sops/day.txt +++ /dev/null @@ -1 +0,0 @@ -day=Tuesday diff --git a/controllers/testdata/sops/day.txt.encrypted b/controllers/testdata/sops/day.txt.encrypted deleted file mode 100644 index 87d91d3c5..000000000 --- a/controllers/testdata/sops/day.txt.encrypted +++ /dev/null @@ -1,20 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:YWPHPTVOCWivqZu0,iv:tLqbJD/KN2BchlAz1mnf4FtMY+SP5hiBYJP6dHy8gtc=,tag:Aj9T0Q7y9baA84EfEt8MfQ==,type:str]", - "sops": { - "kms": null, - "gcp_kms": null, - "azure_kv": null, - "hc_vault": null, - "lastmodified": "2021-04-27T20:27:20Z", - "mac": "ENC[AES256_GCM,data:1OqDvIaUpOKFa1vsa6nc+GHIvsxwQ3JhJsDTp+Yl2r8y0+n0VUbCm9FyqVvq8ur3Y3NyZfX+7FL6HxgTN0RnSMdwK1X16ioGWBk4CM3K7W8tyY7gmhddsuJqSDZdV7Hr2s7FB6LZJAHWO9vTn9zXM75Ef0B5yuOgzp29LmIhCK4=,iv:8ozNZ7IgDub2vICSzHWcAdx7/sVEoe8YayXYrAkN0BM=,tag:UwE0b6eTpA9uir+4Mwed7g==,type:str]", - "pgp": [ - { - "created_at": "2021-04-27T20:27:20Z", - "enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMA90SOJihaAjLAQ//cd4d6zghXW7uJ8rk0PoWiCVy5BeYwnInJT4uqJ5uUY62\nFLlsM4ZJB2SSBHGcXdwkWqTXeLLmD8aEuAe0lfutcOYyMZVWeYY+wybyJ5TgBMAo\nvEJoY67felWRb4h0BzkHIG/ZLiuDTV020GJNH2tGgE/mXVPhYosQ+EmA5EF45vfj\nqx2LjZjsCg28FK2qkXnHHjOV/12OnGpR0y6t9GijBUtttyjYaXUpNUSUiHHMjXyL\nQnKlRPt9N2QF6oUQVEwr9plNYKTfmeqUwWh6wFAaWF/104oSOwXFA8ID5wF6de1j\ntnzVf+1Ld5WNmXGmrz/6ugWfcU/3147EuPodjTyQIFMTxA6V7Z7BORjhuxFpR/jS\noZJF/SS70fg9J7sdizWKFNkqS9pPasdNHcGuXU+KGkD2ya54WyUDE86gMq0xtEf3\nMmQJRnjHuriD5EvnKmDJ+QE9nU0ld0kyfVUueHQHCtuuw7yZGi8vlyyjOq4nqCGV\nZ4TJcmpt7pKoxEAnp2tImnos7DbEoQMl7RIYgrhxS7Nej9naYeadFz/G84uwjfm0\nBr5J3A+xtG37HXQWqtd7EXmy/I94okNVXeAZuuQFt/So78jJ4H9uQK1snukPNBhr\nG8aM8SfdrTbp4KZQpm2RJwNdhbHzHoz2M2Dc6Eo14FceW0R0jYDaKTwKeNIgH6jS\nXgGdX+eJRyC1yhp6HAXOaaR9MvXJ8xCi6clWRpI9h3wxnrZtg+pERFeHhp2Ldlww\nRTjw4g3Cp9GQJB/0aTkVVOPmZ4/jpCyUS6hiV3cEE4veuDYZ20evpgO4sld6Ve8=\n=1o9a\n-----END PGP MESSAGE-----\n", - "fp": "35C1A64CD7FC0AB6EB66756B2445463C3234ECE1" - } - ], - "encrypted_regex": "^(data|stringData)$", - "version": "3.6.0" - } -} \ No newline at end of file diff --git a/controllers/testdata/sops/month/month.yaml b/controllers/testdata/sops/month/month.yaml deleted file mode 100644 index 1467ebc4c..000000000 --- a/controllers/testdata/sops/month/month.yaml +++ /dev/null @@ -1,32 +0,0 @@ -month: ENC[AES256_GCM,data:9e+R,iv:EzJxah6sCY2D9L76l/CuVq6qVq2ncJDYphm9gXE/ZgM=,tag:r82agynzHp/aOTVo6Iu9wg==,type:str] -sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] - age: [] - lastmodified: "2021-05-31T11:27:34Z" - mac: ENC[AES256_GCM,data:BV/jKqSzKr2sq/yA4HToFseOWOB04cYo+54Dby/Jp4ZuVwxNt1i02zncsvWyQZK5WFcvK47brvzN6fWJyyf5WnX+XISbuUDGMWjqNG/te3YKEY4ZqJUopDF/AxDZDkUC5KdnIln6RZqtHuJH18J35kakWFrg1YOJtI28ZVK5yBM=,iv:T6JJkYbfqpUz2AClToZtSsuVbUXcPD5nqaUhJJdH6Uc=,tag:jvmH8iyfivoGIt1k+Uodrg==,type:str] - pgp: - - created_at: "2021-05-31T11:27:34Z" - enc: | - -----BEGIN PGP MESSAGE----- - - hQIMA90SOJihaAjLAQ/9HYs2HyaYL9dOj8zIAr3JzqEFHlMX59Vw8kj9KxBQJXYQ - N3mE/HHQVBWk/36Pq/14n0Eals8GwivDDiJmovfeRASmb0/LnGQDzMkDGEJvyu7N - Q69rBjzVWbmMPgI0vQb0zTBRcUW+LnSijkv+H5mxuFnnZd8N3UeFLHX2oKNeA7O3 - pYjjK8vr6KaXJqYfH+bFs29cnk0+xZiThr21cz40yFZD7ynns4xjdVtqI5bvGk/F - bDW7oGgJe+q/9OHKJaVESLrcZMe2lLxA7x821ssq6BlNzv9DHTc7PloVNepsze6d - MBTgzAZoH04ENQSiL9qo24AVGaFhUXak7MslxE8nhjFJD6sfb0Q/LtlhOSpDw7NR - gugPzQuQLGN9U54id0bql8CBi58g0wdxjo6kDlMYTEd9CZbugfM1pR1imknlgPLi - 7ODDrWTTxnZm4+hZRj7EjMGlRshavPgZ/rgT1tTnjNw9c+llgCWW8Ei8JOEvA86M - DwsPzodesMO56yf3MJPAgakCapTH9VMad+E63yUMsNAX6+otrjgssvxg3j8KnjPp - Z7593P7RGYrRR+YwEi5nTHmDL1H80vP6pNnBGd7wLa3TLzypkDiZSKY6vq6vSIwd - QOpLX3VC2X53mtWmNm7oWxKLX3hKPrjTqBYE0EDK7Yc0q8rj++ygntOekI+WSm/S - XAG4Ufue6i2MTvnZmK/Byt+E/zT4jRmjRQImGekHB+rLYfM3Z85i6ExH4OCCWNqC - rg4DqrWTS8Nvt2PE5UC3Phqe51D4/ZrQPVPkFQftgQl44xECv4X8rI7RTux6 - =HE0m - -----END PGP MESSAGE----- - fp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1 - unencrypted_suffix: _unencrypted - version: 3.7.1 diff --git a/controllers/testdata/sops/month/unencrypted-year.env b/controllers/testdata/sops/month/unencrypted-year.env deleted file mode 100644 index 1583ab226..000000000 --- a/controllers/testdata/sops/month/unencrypted-year.env +++ /dev/null @@ -1 +0,0 @@ -year=2021 diff --git a/controllers/testdata/sops/month/year.env b/controllers/testdata/sops/month/year.env deleted file mode 100644 index ea2167441..000000000 --- a/controllers/testdata/sops/month/year.env +++ /dev/null @@ -1,7 +0,0 @@ -year=ENC[AES256_GCM,data:EfNnlA==,iv:pBaHDmjQ1d6JrA0Rk19giCQon7CP37hZ0dEQTkJEw1U=,tag:J29CEN9S6pSie8tsAD2REA==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3NHYyMHdNcXhMS2x6aXJq\nTHVhbUYrcW8waFduN1NoWUIyTWFuMmNEVGpzCjUxb05zUndSdnpiQng2VnZ2SkNF\nbnlzY0VmaVd1Z0xZR2FKdDRPQlhKSE0KLS0tIDlEaGgwT3VHcUg5QzFpenZNOTBk\nbUZ5QkRnY0kwMFpYanFLYTlvc0FXdXMKb32CnEO8yg91kkUMFXhBL5Sfz32dNOJT\ntNGdKcOGVBzOJVgU1RquB+5OcJdbuwdV7GCq8KvXqh5fypTI00hZeg==\n-----END AGE ENCRYPTED FILE-----\n -sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 -sops_lastmodified=2021-10-14T15:35:45Z -sops_mac=ENC[AES256_GCM,data:brSfy5j0wETn6YT7p8qoCSuI6bevGwrxBbtcqBSYRJ+GgLAr9a7rtwHK8/BnKCi1C1H/zGa1gEERqz2j6Zw0uS4V5lejvtDtfRn9DwYWQ2Aqo2zi4crfNhljerwQVa/Hy9pq2falIZyyhoDX30WOoLe+2eZWQXLtFlVkx4x7U1s=,iv:wr4szytKCN9j6dqccZZl0bkDUHsOtFSvDXjdpuZwTbA=,tag:N1uQ25uLS+E6yQPzXJRiNw==,type:str] -sops_version=3.7.1 -sops_unencrypted_suffix=_unencrypted diff --git a/controllers/testdata/sops/secret.age.yaml b/controllers/testdata/sops/secret.age.yaml deleted file mode 100644 index 17c7172ce..000000000 --- a/controllers/testdata/sops/secret.age.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: sops-age -stringData: - secret: ENC[AES256_GCM,data:RwzrBF8wy16SpfbQoeADeKyz,iv:DuJce2Ebx1Y49DaLCOJ74OOkgiv21roxhz/sZqKCSSs=,tag:Gg9XHapZI5q+rvtgeY6nrg==,type:str] -sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] - age: - - recipient: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNeGduOFZjRWw2WTFQdWdu - OS83OEZaN1E1aU1zSThhMlNEZzd0aEYvdURFCnE3bmJ5c3J2cDNEbXhselFPVC9v - NFhMRjZjOHZOdEpoYjdiS0ZPd2pvN1kKLS0tIDZUVEFoblpDNWhnaWxYRTBjaktk - bHRXV0o1K2ZDNm5Mem5SdzNBMTNuNFUKylE2cRLqydjj6e4+4Giwn4y8mIPej+CM - Bab3UWiK1da2rFNTOEnoHl6QDAVxNrWdrrIa5k22SzApT88VtJ4xuQ== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2021-04-06T09:07:05Z" - mac: ENC[AES256_GCM,data:oaM8qFtEP8dOCd/Tr5yb08uetsnDtZO8o1rCayN53ncQ1HUAdhRBrFdmbYx1YTh1mwQVVN6sGYqFZU1LBMVv5pTqvpwd41biJZEg8NznXQWx0GA2Z6HOrblGhFZKrqky3P5xN+6j63zkJizXWgBMKzRvBnsVKxjZGr/lk1vVVv4=,iv:p4y9Fo3SArkEMuoK2d9sQYgNdc0iw/StFhg/5LnhcXM=,tag:61JGbnEw35tv6WnGj46JOw==,type:str] - pgp: [] - encrypted_regex: ^(data|stringData)$ - version: 3.7.0 diff --git a/controllers/testdata/sops/secret.day.yaml b/controllers/testdata/sops/secret.day.yaml deleted file mode 100644 index 19d62784e..000000000 --- a/controllers/testdata/sops/secret.day.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -data: - secret: ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpCZDJuL1VCc21KN2NYYXVvLGl2OmR4c25ncWVDVitzVVVIM24rVTZ1N1F3WjEvRFptM3RhVlVOSjJRTFNlWGM9LHRhZzp3enhxUjA2MWRmbmVhQmlMNHRxaTN3PT0sdHlwZTpzdHJdIiwKCSJzb3BzIjogewoJCSJrbXMiOiBudWxsLAoJCSJnY3Bfa21zIjogbnVsbCwKCQkiYXp1cmVfa3YiOiBudWxsLAoJCSJoY192YXVsdCI6IG51bGwsCgkJImxhc3Rtb2RpZmllZCI6ICIyMDIxLTA0LTI3VDE5OjQ4OjIwWiIsCgkJIm1hYyI6ICJFTkNbQUVTMjU2X0dDTSxkYXRhOkUwQmFsdjRGcWRiQVNRSGNpa2oyaURTeTdCTjBVSUY3cHhlSFNwUUZ2dXF6akVtRWlDV2xJRFl0dmgxY2t4ZnZxNHpYS2xITkp6QitkM1RSaHNuaGtIZ2tSWFA1MldwUkNHZ1pwY3h5Q3FCTmhhL00wRGNGY1ZZZG14T2NVNEU4eFdsdFRuektzZll0bVkvTVludmVJT1htNkpmMUhPS2FVM1EwdzBGbG8wQT0saXY6QWgza0puZEI3UnE5RVV3ZUc0TUMzUmh5bXRYRXY0ellIc2M3M3NxQzlGdz0sdGFnOnpOS3ZHcW5WNG8yWTdhNUJTV28yZEE9PSx0eXBlOnN0cl0iLAoJCSJwZ3AiOiBbCgkJCXsKCQkJCSJjcmVhdGVkX2F0IjogIjIwMjEtMDQtMjdUMTk6NDg6MjBaIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tXG5cbmhRSU1BOTBTT0ppaGFBakxBUS8vZGo2NzlnSXpKdU1vc05wdkRZQVNUOVF4MjBGdmJOREsvTDhmY2xlTFd0NHpcbktWaHdlTnRRdXhZbXA1a2Z6VUpBWXh2Mk01a2NSWFo1QzVDWDJLSER2N1lxVWRRdVdMam9wTFRmR1RPcE56RlBcbjNyZE5sRXZKMC8zSXdteEhtYXVPbUpPazByRUQrOXZtTXNCL3pnaXIxWkd2d0tTNVM5dW9XSDN6bUlVdmJBR29cbmpCKzAxRkdqaXlYRUhSanFzbFlMZ0tXczhZaVR4anNPOVd3QlU0ZmRySzRCKytEaWFweVdFcDJYVWhFZWt6MjFcbjR3dXU5dEp5TmtOWXpmMXNXUWxMc1lPblMzRkkrNSsrMFg1SmREYWtFVmkzSnF1TmtLeDRuWms3ZHJqcktnM1hcbnJPWTc1YnIxdGkwTkxrQWFsaUwvbEJSR3JCTW5BeTViV0l4dkpoSmtqRGMyZTNBWmJNbXdJQ0FJZ05kMEcrNFBcbkJqWkhNWnZUQk1RN0VvWFhGeVg5K3JKKzFnUzF5UEJuaFNma3lrSmJSR1ljdnB1RVRFL1NyK0FSa0s0cHV5bFVcbk5sdU8xZmdOMEF1STVqVll2NzJ1MzJWZEw2N2ZYbjlPdjhFYmlkdVVKcWoxZXB3Mk53dDNZK2xrNERLbVBybVRcbjFRTzF3OC96UHo1SlR0U1R4ZXFJak4weXBTazFocE9XekNwOTE0QmgxckFscXFxakorc0Q4dkVseEk2N2JSWG5cblY1alBkZkQwQktLU0tqS0ZLeVhnUHdPdCtvd2xTTDROR0V6bmdTcmsyeDlTcHVDdWQweXpoeVpta2tHRm5JKzdcbmhpT2kzeGxmZnkvRWY4TDkvaWhDbmJQc1pTck50L0RPQlVGK0ZGQUlmZitpUElPRTBieGZCaHpMNWZOTS9ZdlNcblhBRS9pYk42NktLT2ZwYWlqWnRXSkdTY1RHVVlYMkt2WTAwN2h6Y1ErR3BaZUZOd3oyUlpEd3BkTzZ4N3JHelFcbkFHQUtjd2pTYXcramluVzQwWmZnOWQ5YmFMdWRYTDRXVU9FSUdTN2FpWjNFNjJTSFJGU2U0dmNpSVh6blxuPThRcmZcbi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS1cbiIsCgkJCQkiZnAiOiAiMzVDMUE2NENEN0ZDMEFCNkVCNjY3NTZCMjQ0NTQ2M0MzMjM0RUNFMSIKCQkJfQoJCV0sCgkJImVuY3J5cHRlZF9yZWdleCI6ICJeKGRhdGF8c3RyaW5nRGF0YSkkIiwKCQkidmVyc2lvbiI6ICIzLjYuMCIKCX0KfQ== -kind: Secret -metadata: - creationTimestamp: null - name: sops-day diff --git a/controllers/testdata/sops/secret.vault.yaml b/controllers/testdata/sops/secret.vault.yaml deleted file mode 100644 index 71d476b4c..000000000 --- a/controllers/testdata/sops/secret.vault.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -data: - secret: bXktc29wcy12YXVsdC1zZWNyZXQK -kind: Secret -metadata: - name: sops-hcvault - namespace: default -type: Opaque diff --git a/controllers/testdata/sops/secret.yaml b/controllers/testdata/sops/secret.yaml deleted file mode 100644 index c6aa991c7..000000000 --- a/controllers/testdata/sops/secret.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: sops-pgp -stringData: - secret: ENC[AES256_GCM,data:rZEmadbj49GoQLlK85hKKAsc,iv:FX4Dfbd173bZQdUgEVRo4q29m/Gz9ob07QHFuiCAufA=,tag:VM6tzAVdGjsythy2Mr5tvw==,type:str] -sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] - age: [] - lastmodified: "2021-04-06T09:07:19Z" - mac: ENC[AES256_GCM,data:iBg8FY39VSykcWZ/asv86P3VNZkscQdINNOy3UtI5m4OWDpUkyDuq66w7ELiiEXJ3D+b7JKJrsSrYtT7Tn7t+NZGxJcLQFEczozvWgKd2hCikxnMEepCJ3tRcoz7JaItommi1HvA08syGfLA5f6eOxsHQWzmjVdYaVpQ4VGRibk=,iv:VI+Fb7dXV4442IMKZSHOb0GJ/2nNgK9AUTblOZ49Oco=,tag:gJjFguJeE7irKZW7yZi0jw==,type:str] - pgp: - - created_at: "2021-04-06T09:07:19Z" - enc: | - -----BEGIN PGP MESSAGE----- - - hQIMA90SOJihaAjLAQ/+LnZo9UHmJ2Llcpq6m5gjo5hbCx6aYTbrvJOFCWeu2oyC - 71XsuTUzBp7TK8SkGrxlJmUodezACQ3rCsKY/r2GI4t9HkVRSuhnc/YQMunm3iG1 - bsgfdV/KBm0Go7dFXy2R1Pt3PuVnuM9MZ59U4SdqYGZDI7vzy2gfH127qa3oIOoF - 2OFfwhUy8nZIVCJ47ExIdrc7Qdk94tbLfwmBAKHFN4Ab0YXasKCpH9O+9/vQ+JJU - 7xy61Nv4dqtEDYU9QTh2ZuT6ZaWikTqCcIv/W7lW1RsT8n7YiRZv9POobKDh5KbP - PyfqvJsLcJB8LHN2kZfwr6Iemuce19kRi+7JL9zMGRJSsq0thJ0ly3JBi48pU27w - jbFnmxlIwfb0EsLBp9lsxw7GoUbooSC/rfI5NVeQ+4lFA4gQn2oz7i4zTYesnwil - lrgMxz49SSluAYsGjrJHc+ABmlDz83K42KtWlNjwaIbDgHMl4EbYUe4pxcynEZ6D - 0csDIsIA15MP0THfTL1F1vkhvdPHNuUlVjFqgWaJAP2CC5KH8IeTCUN72FySEYAB - BJH+VQoRnS942M8VQAfUQyBsfZKtQhyCkU7KEimUjQzy75JWgy8YMX1mviXk52qB - kVHQIjNEuBta58pmNyhxc+6+bz+ABGp+mR9QemUQjmXghH3VjOwnZVj6KMMX4J3S - XgEubPmw6u4nYqb9bLDVyE2uXXA4TVgFDuZxJrbZOn9zF2aQOOGfZX2Gx5xgK+pV - srM1wyJqdP+QL/fWO9ZI38+tyr1T5zOBPpJ/JTrkSJoVeRWpwuI6BUCZhH66nfU= - =+1cf - -----END PGP MESSAGE----- - fp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1 - encrypted_regex: ^(data|stringData)$ - version: 3.7.0 diff --git a/controllers/testdata/test-dotenv/bases/secrets/year2.txt b/controllers/testdata/test-dotenv/bases/secrets/year2.txt deleted file mode 100644 index ed82abf90..000000000 --- a/controllers/testdata/test-dotenv/bases/secrets/year2.txt +++ /dev/null @@ -1,7 +0,0 @@ -year=ENC[AES256_GCM,data:HoFRvaM=,iv:XNDFLkONNvKSKkbqErVx1/tnEtDuZIG3SficCd7NIaM=,tag:aC7SCerL01kYyXyXkWR2ag==,type:str] -sops_unencrypted_suffix=_unencrypted -sops_mac=ENC[AES256_GCM,data:s75x7NzSjmkovCOopnT1eIfXMAdwwsN8KoVdVbAYDTAsB856w/i/W/JshXAUdr5SnXHNbtwzEha/HSppnWEQw1nds18yZCeIW54QE7yxvBKw9Mhd3wxHWiZWziTY0awbYinbyQ45zpq1Iz97BueNjhwtZWMQzRKLQvwyqEljTHs=,iv:AuKqCzIgTYcogtyLrtM6VdgwKTlDE3uMxvVaWbpKBOA=,tag:Ija+U/97TxxWoXYDpG6+jg==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYV1FYTkdzV210SkswWmty\nZzZSVzlCUlRQcVNEOVpYSWNSSWtPd00rcDJrCjZKVVp6aFY2cHJQbm9oY2Q1Z2N3\nLzBWalF4ZHZYTU5kMlcwaGRvYkVKcFEKLS0tIG1QTjNuY0pRbFBqT3dFNFROQWU3\nTWQxNVlUNG8rblQyYmJoaCtKSGcrdE0KjUJ+hGiyCkzUG41mwT3rAb0BdwBF8303\nhBDRmW+DjP1ETrGTXviTS1Cq29IX1K2KdBRxixjtwewkXV/i87wHRA==\n-----END AGE ENCRYPTED FILE-----\n -sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 -sops_lastmodified=2021-10-15T11:09:14Z -sops_version=3.7.1 diff --git a/controllers/testdata/test-dotenv/overlays/year1.env b/controllers/testdata/test-dotenv/overlays/year1.env deleted file mode 100644 index 71e01927c..000000000 --- a/controllers/testdata/test-dotenv/overlays/year1.env +++ /dev/null @@ -1,7 +0,0 @@ -year=ENC[AES256_GCM,data:tV/GLTE=,iv:AtEKKSUa4BiTnDzGMtpGrO78NuR0wMXzjKrQScbtX24=,tag:zAzcBzQ6ORO+NhcY3idHcA==,type:str] -sops_lastmodified=2021-10-15T11:08:51Z -sops_unencrypted_suffix=_unencrypted -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFU29oWEh2ckRjaCs3d1FJ\nYTkxN0dqY1lsc1dEUmZ4OGN0N1BHK0xxQld3CmpTL2Z2VDloQStCYnRmYnJ0SDFj\nVU9USmszbU44YUxzRi95Q0sxY2t0bkUKLS0tIC80Ulh1RWJPeUFqbUFNSjFOeGIy\nY001MzMwbnRsQXlsN1VVY2xLY20yazQKYhZQGZpay9J1cnGiHCKBY6DtYMCSIBo7\nAP41GiVukT6M4LT83TpWzWgbR/xNgreKdNpweYcw+Fp+wJHVeR3+fg==\n-----END AGE ENCRYPTED FILE-----\n -sops_mac=ENC[AES256_GCM,data:rw8vAq+8nqa5/V8p/ICuVKXNQCeTIFExF33qy1YEbc8f4kePDhTlGqxluEytbWOhk+hzCd4POk+zY8bWBY2QSiq0lle2rCtE2WT3I04/+bHzX74yMBuadYLqiUFEhkra/58FXD404PPJBUrOy8mAPgWVczcqMexYhzz//tPdGMY=,iv:yk3CsyGigCSHonvMBTQvjg+kgNssf87KqlKeR6FE8sk=,tag:dCaOhh97ebJWNT5v35n6Iw==,type:str] -sops_version=3.7.1 -sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 diff --git a/docs/api/kustomize.md b/docs/api/v1/kustomize.md similarity index 70% rename from docs/api/kustomize.md rename to docs/api/v1/kustomize.md index bd2c8cd51..a109c61e5 100644 --- a/docs/api/kustomize.md +++ b/docs/api/v1/kustomize.md @@ -1,17 +1,18 @@ -

Kustomize API reference

+

Kustomize API reference v1

Packages:

-

kustomize.toolkit.fluxcd.io/v1beta2

-

Package v1beta2 contains API Schema definitions for the kustomize.toolkit.fluxcd.io v1beta2 API group.

+

kustomize.toolkit.fluxcd.io/v1

+

Package v1 contains API Schema definitions for the kustomize.toolkit.fluxcd.io +v1 API group.

Resource Types: -

Kustomization +

Kustomization

Kustomization is the Schema for the kustomizations API.

@@ -29,7 +30,7 @@ Resource Types: apiVersion
string -kustomize.toolkit.fluxcd.io/v1beta2 +kustomize.toolkit.fluxcd.io/v1 @@ -59,7 +60,7 @@ Refer to the Kubernetes API documentation for the fields of the spec
- + KustomizationSpec @@ -70,6 +71,22 @@ KustomizationSpec + + + + @@ -165,7 +184,7 @@ Defaults to ‘None’, which translates to the root path of the SourceR + + + + @@ -279,7 +307,7 @@ when reconciling this Kustomization.

+commonMetadata
+ + +CommonMetadata + + +
+(Optional) +

CommonMetadata specifies the common labels and annotations that are +applied to all resources. Any existing label or annotation will be +overridden if its key matches a common one.

+
dependsOn
@@ -88,7 +105,7 @@ Kustomization can be reconciled.

decryption
- + Decryption @@ -108,7 +125,9 @@ Kubernetes meta/v1.Duration
-

The interval at which to reconcile the Kustomization.

+

The interval at which to reconcile the Kustomization. +This interval is approximate and may be subject to jitter to ensure +efficient use of resources.

postBuild
- + PostBuild @@ -189,6 +208,21 @@ bool
+deletionPolicy
+ +string + +
+(Optional) +

DeletionPolicy can be used to control garbage collection when this +Kustomization is deleted. Valid values are (‘MirrorPrune’, ‘Delete’, +‘WaitForTermination’, ‘Orphan’). ‘MirrorPrune’ mirrors the Prune field +(orphan if false, delete if true). Defaults to ‘MirrorPrune’.

+
healthChecks
@@ -203,47 +237,41 @@ bool
-patches
+namePrefix
- -[]github.com/fluxcd/pkg/apis/kustomize.Patch - +string
(Optional) -

Strategic merge and JSON patches, defined as inline YAML objects, -capable of targeting objects based on kind, label and annotation selectors.

+

NamePrefix will prefix the names of all managed resources.

-patchesStrategicMerge
+nameSuffix
- -[]Kubernetes pkg/apis/apiextensions/v1.JSON - +string
(Optional) -

Strategic merge patches, defined as inline YAML objects. -Deprecated: Use Patches instead.

+

NameSuffix will suffix the names of all managed resources.

-patchesJson6902
+patches
- -[]github.com/fluxcd/pkg/apis/kustomize.JSON6902Patch + +[]github.com/fluxcd/pkg/apis/kustomize.Patch
(Optional) -

JSON 6902 patches, defined as inline YAML objects. -Deprecated: Use Patches instead.

+

Strategic merge and JSON patches, defined as inline YAML objects, +capable of targeting objects based on kind, label and annotation selectors.

sourceRef
- + CrossNamespaceSourceReference @@ -351,32 +379,36 @@ bool
(Optional) -

Wait instructs the controller to check the health of all the reconciled resources. -When enabled, the HealthChecks are ignored. Defaults to false.

+

Wait instructs the controller to check the health of all the reconciled +resources. When enabled, the HealthChecks are ignored. Defaults to false.

-validation
+components
-string +[]string
(Optional) -

Deprecated: Not used in v1beta2.

+

Components specifies relative paths to specifications of other Components.

-components
+healthCheckExprs
-[]string + +[]github.com/fluxcd/pkg/apis/kustomize.CustomHealthCheck +
(Optional) -

Components specifies relative paths to specifications of other Components

+

HealthCheckExprs is a list of healthcheck expressions for evaluating the +health of custom resources using Common Expression Language (CEL). +The expressions are evaluated only when Wait or HealthChecks are specified.

@@ -386,7 +418,7 @@ string status
- + KustomizationStatus @@ -398,11 +430,56 @@ KustomizationStatus
-

CrossNamespaceSourceReference +

CommonMetadata

(Appears on: -KustomizationSpec) +KustomizationSpec) +

+

CommonMetadata defines the common labels and annotations.

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+annotations
+ +map[string]string + +
+(Optional) +

Annotations to be added to the object’s metadata.

+
+labels
+ +map[string]string + +
+(Optional) +

Labels to be added to the object’s metadata.

+
+
+
+

CrossNamespaceSourceReference +

+

+(Appears on: +KustomizationSpec)

CrossNamespaceSourceReference contains enough information to let you locate the typed Kubernetes resource object at cluster level.

@@ -459,18 +536,19 @@ string (Optional) -

Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference.

+

Namespace of the referent, defaults to the namespace of the Kubernetes +resource object that contains the reference.

-

Decryption +

Decryption

(Appears on: -KustomizationSpec) +KustomizationSpec)

Decryption defines how decryption is handled for Kubernetes manifests.

@@ -496,6 +574,22 @@ string +serviceAccountName
+ +string + + + +(Optional) +

ServiceAccountName is the name of the service account used to +authenticate with KMS services from cloud providers. If a +static credential for a given cloud provider is defined +inside the Secret referenced by SecretRef, that static +credential takes priority.

+ + + + secretRef
@@ -505,20 +599,24 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference (Optional) -

The secret name containing the private OpenPGP keys used for decryption.

+

The secret name containing the private OpenPGP keys used for decryption. +A static credential for a cloud provider defined inside the Secret +takes priority to secret-less authentication with the ServiceAccountName +field.

-

KustomizationSpec +

KustomizationSpec

(Appears on: -Kustomization) +Kustomization)

-

KustomizationSpec defines the configuration to calculate the desired state from a Source using Kustomize.

+

KustomizationSpec defines the configuration to calculate the desired state +from a Source using Kustomize.

@@ -531,6 +629,22 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference + + + + @@ -626,7 +742,7 @@ Defaults to ‘None’, which translates to the root path of the SourceR + + + + @@ -740,7 +865,7 @@ when reconciling this Kustomization.

+commonMetadata
+ + +CommonMetadata + + +
+(Optional) +

CommonMetadata specifies the common labels and annotations that are +applied to all resources. Any existing label or annotation will be +overridden if its key matches a common one.

+
dependsOn
@@ -549,7 +663,7 @@ Kustomization can be reconciled.

decryption
- + Decryption @@ -569,7 +683,9 @@ Kubernetes meta/v1.Duration
-

The interval at which to reconcile the Kustomization.

+

The interval at which to reconcile the Kustomization. +This interval is approximate and may be subject to jitter to ensure +efficient use of resources.

postBuild
- + PostBuild @@ -650,6 +766,21 @@ bool
+deletionPolicy
+ +string + +
+(Optional) +

DeletionPolicy can be used to control garbage collection when this +Kustomization is deleted. Valid values are (‘MirrorPrune’, ‘Delete’, +‘WaitForTermination’, ‘Orphan’). ‘MirrorPrune’ mirrors the Prune field +(orphan if false, delete if true). Defaults to ‘MirrorPrune’.

+
healthChecks
@@ -664,47 +795,41 @@ bool
-patches
+namePrefix
- -[]github.com/fluxcd/pkg/apis/kustomize.Patch - +string
(Optional) -

Strategic merge and JSON patches, defined as inline YAML objects, -capable of targeting objects based on kind, label and annotation selectors.

+

NamePrefix will prefix the names of all managed resources.

-patchesStrategicMerge
+nameSuffix
- -[]Kubernetes pkg/apis/apiextensions/v1.JSON - +string
(Optional) -

Strategic merge patches, defined as inline YAML objects. -Deprecated: Use Patches instead.

+

NameSuffix will suffix the names of all managed resources.

-patchesJson6902
+patches
- -[]github.com/fluxcd/pkg/apis/kustomize.JSON6902Patch + +[]github.com/fluxcd/pkg/apis/kustomize.Patch
(Optional) -

JSON 6902 patches, defined as inline YAML objects. -Deprecated: Use Patches instead.

+

Strategic merge and JSON patches, defined as inline YAML objects, +capable of targeting objects based on kind, label and annotation selectors.

sourceRef
- + CrossNamespaceSourceReference @@ -812,43 +937,47 @@ bool
(Optional) -

Wait instructs the controller to check the health of all the reconciled resources. -When enabled, the HealthChecks are ignored. Defaults to false.

+

Wait instructs the controller to check the health of all the reconciled +resources. When enabled, the HealthChecks are ignored. Defaults to false.

-validation
+components
-string +[]string
(Optional) -

Deprecated: Not used in v1beta2.

+

Components specifies relative paths to specifications of other Components.

-components
+healthCheckExprs
-[]string + +[]github.com/fluxcd/pkg/apis/kustomize.CustomHealthCheck +
(Optional) -

Components specifies relative paths to specifications of other Components

+

HealthCheckExprs is a list of healthcheck expressions for evaluating the +health of custom resources using Common Expression Language (CEL). +The expressions are evaluated only when Wait or HealthChecks are specified.

-

KustomizationStatus +

KustomizationStatus

(Appears on: -Kustomization) +Kustomization)

KustomizationStatus defines the observed state of a kustomization.

@@ -911,7 +1040,23 @@ string (Optional)

The last successfully applied revision. -The revision format for Git sources is /.

+Equals the Revision of the applied Artifact from the referenced Source.

+ + + + +lastAppliedOriginRevision
+ +string + + + +(Optional) +

The last successfully applied origin revision. +Equals the origin revision of the applied Artifact from the referenced Source. +Usually present on the Metadata of the applied Artifact and depends on the +Source type, e.g. for OCI it’s the value associated with the key +“org.opencontainers.image.revision”.

@@ -930,25 +1075,26 @@ string inventory
- + ResourceInventory (Optional) -

Inventory contains the list of Kubernetes resource object references that have been successfully applied.

+

Inventory contains the list of Kubernetes resource object references that +have been successfully applied.

-

PostBuild +

PostBuild

(Appears on: -KustomizationSpec) +KustomizationSpec)

PostBuild describes which actions to perform on the YAML manifest generated by building the kustomize overlay.

@@ -972,9 +1118,8 @@ map[string]string (Optional)

Substitute holds a map of key/value pairs. -The variables defined in your YAML manifests -that match any of the keys defined in the map -will be substituted with the set value. +The variables defined in your YAML manifests that match any of the keys +defined in the map will be substituted with the set value. Includes support for bash string replacement functions e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.

@@ -983,7 +1128,7 @@ e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.

substituteFrom
- + []SubstituteReference @@ -992,21 +1137,23 @@ e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.

(Optional)

SubstituteFrom holds references to ConfigMaps and Secrets containing the variables and their values to be substituted in the YAML manifests. -The ConfigMap and the Secret data keys represent the var names and they -must match the vars declared in the manifests for the substitution to happen.

+The ConfigMap and the Secret data keys represent the var names, and they +must match the vars declared in the manifests for the substitution to +happen.

-

ResourceInventory +

ResourceInventory

(Appears on: -KustomizationStatus) +KustomizationStatus)

-

ResourceInventory contains a list of Kubernetes resource object references that have been applied by a Kustomization.

+

ResourceInventory contains a list of Kubernetes resource object references +that have been applied by a Kustomization.

@@ -1021,7 +1168,7 @@ must match the vars declared in the manifests for the substitution to happen.

entries
- + []ResourceRef @@ -1034,11 +1181,11 @@ must match the vars declared in the manifests for the substitution to happen.

-

ResourceRef +

ResourceRef

(Appears on: -ResourceInventory) +ResourceInventory)

ResourceRef contains the information necessary to locate a resource within a cluster.

@@ -1078,11 +1225,11 @@ string
-

SubstituteReference +

SubstituteReference

(Appears on: -PostBuild) +PostBuild)

SubstituteReference contains a reference to a resource containing the variables name and value.

diff --git a/docs/spec/README.md b/docs/spec/README.md index c980280b1..166d7311a 100644 --- a/docs/spec/README.md +++ b/docs/spec/README.md @@ -1,53 +1,7 @@ # Kustomize Controller -The kustomize-controller is a Kubernetes operator, specialized in running -continuous delivery pipelines for infrastructure and workloads -defined with Kubernetes manifests and assembled with Kustomize. +## API Specification -## Motivation - -The main goal is to provide an automated operator that can -bootstrap and continuously reconcile the cluster state -from multiple sources (e.g. infrastructure and application repositories). - -When provisioning a new cluster, one may wish to install workloads in a specific order, -for example a validation controller such as OPA Gatekeeper should be up and running before -applying other manifests on the cluster. Another example is a service mesh admission controller, -the proxy injector must be functional before deploying applications into the mesh. - -When a cluster is shared with multiple teams, a cluster admin may wish to assign roles and service -accounts to each team. The manifests owned by a team will be applied on the cluster using -the team's account thus ensuring isolation between teams. For example, an admin can -restrict the operations performed on the cluster by a team to a single namespace. - -When dealing with an incident, one may wish to suspend the reconciliation of some workloads and -pin the reconciliation of others to a specific Git revision, without having to stop the reconciler -and affect the whole cluster. - -When operating a cluster, different teams may wish to receive notification about the status -of their CD pipelines. For example, the on-call team would receive alerts about all -failures in the prod namespace, while the frontend team may wish to be alerted when a new version -of the frontend app was deployed and if the deployment is healthy, no matter the namespace. - -## Design - -The reconciliation process can be defined with a Kubernetes custom resource -that describes a pipeline such as: -- **check** if depends-on conditions are meet -- **fetch** manifests from source-controller -- **generate** `kustomization.yaml` if needed -- **build** the manifests using the Kustomize SDK -- **decrypt** Kubernetes secrets using Mozilla SOPS SDK -- **impersonate** the tenant's Kubernetes account -- **validate** the resulting objects using server-side apply dry-run -- **detect drift** between the desired and state and cluster state -- **correct drift** by applying the objects using server-side apply -- **prune** the objects removed from source -- **wait** for the applied changes to rollout using Kubernetes kstatus library -- **report** the reconciliation result in the `status` sub-resource -- **alert** if something went wrong by sending events to Kubernetes API and notification-controller -- **notify** if the cluster state changed by sending events to Kubernetes API and notification-controller - -## Specifications - -The latest API specifications can be found [here](v1beta2/README.md). +[v1beta1](v1beta2/README.md). +[v1beta2](v1beta2/README.md). +[v1](v1/README.md). diff --git a/docs/spec/v1/README.md b/docs/spec/v1/README.md new file mode 100644 index 000000000..a06b0319a --- /dev/null +++ b/docs/spec/v1/README.md @@ -0,0 +1,17 @@ +# kustomize.toolkit.fluxcd.io/v1 + +This is the v1 API specification for defining continuous delivery pipelines +of Kubernetes objects generated with Kustomize. + +## Specification + +- [Kustomization CRD](kustomizations.md) + + [Example](kustomizations.md#example) + + [Writing a Kustomization spec](kustomizations.md#writing-a-kustomization-spec) + + [Working with Kustomizations](kustomizations.md#working-with-kustomizations) + * [Recommended settings](kustomizations.md#recommended-settings) + + [Kustomization Status](kustomizations.md#kustomization-status) + +## Implementation + +* [kustomize-controller](https://github.com/fluxcd/kustomize-controller/) diff --git a/docs/spec/v1/kustomizations.md b/docs/spec/v1/kustomizations.md new file mode 100644 index 000000000..537bb800d --- /dev/null +++ b/docs/spec/v1/kustomizations.md @@ -0,0 +1,2039 @@ +# Kustomization + + + +The `Kustomization` API defines a pipeline for fetching, decrypting, building, +validating and applying Kustomize overlays or plain Kubernetes manifests. +The `Kustomization` Custom Resource Definition is the +counterpart of Kustomize's `kustomization.yaml` config file. + +## Example + +The following is an example of a Flux Kustomization that reconciles the +Kubernetes manifests stored in a Git repository. + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: default +spec: + interval: 5m + url: https://github.com/stefanprodan/podinfo + ref: + branch: master +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: default +spec: + interval: 10m + targetNamespace: default + sourceRef: + kind: GitRepository + name: podinfo + path: "./kustomize" + prune: true + timeout: 1m +``` + +In the above example: + +- A Flux GitRepository named `podinfo` is created that clones the `master` + branch and makes the repository content available as an Artifact inside the cluster. +- A Flux Kustomization named `podinfo` is created that watches the + GitRepository for Artifact changes. +- The Kustomization builds the YAML manifests located at the specified `.spec.path`, + sets the namespace of all objects to the `.spec.targetNamespace`, + validates the objects against the Kubernetes API and finally applies them on + the cluster. +- As specified by `.spec.interval`, every ten minutes, the Kustomization runs a + server-side apply dry-run to detect and correct drift inside the cluster. +- When the Git revision changes, the manifests are reconciled automatically. If + previously applied objects are missing from the current revision, these + objects are deleted from the cluster when `.spec.prune` is enabled. + +You can run this example by saving the manifest into `podinfo.yaml`. + +1. Apply the resource on the cluster: + + ```sh + kubectl apply -f podinfo.yaml + ``` + +2. Run `kubectl get gitrepositories` to see the source status: + + ```console + NAME URL READY STATUS + podinfo https://github.com/stefanprodan/podinfo True stored artifact for revision 'master@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0' + ``` + +3. Run `kubectl get kustomizations` to see the reconciliation status: + + ```console + NAME READY STATUS + podinfo True Applied revision: master@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 + ``` + +4. Run `kubectl describe kustomization podinfo` to see the reconciliation status + conditions and events: + + ```console + ... + Status: + Conditions: + Last Transition Time: 2023-03-07T11:14:41Z + Message: Applied revision: master@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 + Reason: ReconciliationSucceeded + Status: True + Type: Ready + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Progressing 1m48s kustomize-controller Service/default/podinfo created + Deployment/default/podinfo created + HorizontalPodAutoscaler/default/podinfo created + Normal ReconciliationSucceeded 1m48s kustomize-controller Reconciliation finished in 176.163666ms, next run in 10m0s + ``` + +## Writing a Kustomization spec + +As with all other Kubernetes config, a Kustomization needs `apiVersion`, +`kind`, and `metadata` fields. The name of a Kustomization object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). + +A Kustomization also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Source reference + +`.spec.sourceRef` is used to refer to the Source object which has the required +Artifact containing the YAML manifests. It has two required fields: + +- `kind`: The Kind of the referred Source object. Supported Source types: + + [GitRepository](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/gitrepositories.md) + + [OCIRepository](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/ocirepositories.md) + + [Bucket](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/buckets.md) +- `name`: The Name of the referred Source object. + +#### Cross-namespace references + +By default, the Source object is assumed to be in the same namespace as the +Kustomization. To refer to a Source object in a different namespace, specify +the namespace using `.spec.sourceRef.namespace`. + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: webapp + namespace: apps +spec: + interval: 5m + path: "./deploy" + sourceRef: + kind: GitRepository + name: webapp + namespace: shared +``` + +On multi-tenant clusters, platform admins can disable cross-namespace references +by starting kustomize-controller with the `--no-cross-namespace-refs=true` flag. + +### Prune + +`.spec.prune` is a required boolean field to enable/disable garbage collection +for a Kustomization. + +Garbage collection means that the Kubernetes objects that were previously +applied on the cluster but are missing from the current source revision, are +removed from the cluster automatically. Garbage collection is also performed +when a Kustomization object is deleted, triggering a removal of all Kubernetes +objects previously applied on the cluster. The removal of the Kubernetes +objects is done in the background, i.e. it doesn't block the reconciliation of +the Kustomization. + +To enable garbage collection for a Kustomization, set this field to `true`. + +You can disable pruning for certain resources by either labelling or +annotating them with: + +```yaml +kustomize.toolkit.fluxcd.io/prune: disabled +``` + +For details on how the controller tracks Kubernetes objects and determines what +to garbage collect, see [`.status.inventory`](#inventory). + +### Deletion policy + +`.spec.deletionPolicy` is an optional field that allows control over +garbage collection when a Kustomization object is deleted. The default behavior +is to mirror the configuration of [`.spec.prune`](#prune). + +Valid values: + +- `MirrorPrune` (default) - The managed resources will be deleted if `prune` is + `true` and orphaned if `false`. +- `Delete` - Ensure the managed resources are deleted before the Kustomization + is deleted. +- `WaitForTermination` - Ensure the managed resources are deleted and wait for + termination before the Kustomization is deleted. +- `Orphan` - Leave the managed resources when the Kustomization is deleted. + +The `WaitForTermination` deletion policy blocks and waits for the managed +resources to be removed from etcd by the Kubernetes garbage collector. +The wait time is determined by the `.spec.timeout` field. If a timeout occurs, +the controller will stop waiting for the deletion of the resources, +log an error and will allow the Kustomization to be deleted. + +For special cases when the managed resources are removed by other means (e.g. +the deletion of the namespace specified with +[`.spec.targetNamespace`](#target-namespace)), you can set the deletion policy +to `Orphan`: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app + namespace: default +spec: + # ...omitted for brevity + targetNamespace: app-namespace + prune: true + deletionPolicy: Orphan +``` + +### Interval + +`.spec.interval` is a required field that specifies the interval at which the +Kustomization is reconciled, i.e. the controller fetches the source with the +Kubernetes manifests, builds the Kustomization and applies it on the cluster, +correcting any existing drift in the process. The minimum value should be 60 +seconds. + +After successfully reconciling the object, the controller requeues it for +inspection after the specified interval. The value must be in a +[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration), +e.g. `10m0s` to reconcile the object every 10 minutes. + +If the `.metadata.generation` of a resource changes (due to e.g. a change to +the spec) or the Source revision changes (which generates a Kubernetes event), +this is handled instantly outside the interval window. + +**Note:** The controller can be configured to apply a jitter to the interval in +order to distribute the load more evenly when multiple Kustomization objects are +set up with the same interval. For more information, please refer to the +[kustomize-controller configuration options](https://fluxcd.io/flux/components/kustomize/options/). + +### Retry interval + +`.spec.retryInterval` is an optional field to specify the interval at which to +retry a failed reconciliation. Unlike `.spec.interval`, this field is +exclusively meant for failure retries. If not specified, it defaults to +`.spec.interval`. + +### Path + +`.spec.path` is an optional field to specify the path to the directory in the +Source Artifact containing the `kustomization.yaml` file, or the set of plain +YAMLs for which a `kustomization.yaml` should be generated. +It defaults to blank, which translates to the root of the Source Artifact. + +For more details on the generation of the file, see [generating a +`kustomization.yaml` file](#generating-a-kustomizationyaml-file). + +### Target namespace + +`.spec.targetNamespace` is an optional field to specify the target namespace for +all the objects that are part of the Kustomization. It either configures or +overrides the [Kustomize `namespace`](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/namespace/). + +While `.spec.targetNamespace` is optional, if this field is non-empty then the +Kubernetes namespace being pointed to must exist prior to the Kustomization +being applied or be defined by a manifest included in the Kustomization. +kustomize-controller will not create the namespace automatically. + +### Suspend + +`.spec.suspend` is an optional boolean field to suspend the reconciliation of the +Kustomization. When a Kustomization is suspended, new Source revisions are not +applied to the cluster and drift detection/correction is paused. +To resume normal reconciliation, set it back to `false` or remove the field. + +For more information, see [suspending and resuming](#suspending-and-resuming). + +### Health checks + +`.spec.healthChecks` is an optional list used to refer to resources for which the +controller will perform health checks used to determine the rollout status of +[deployed workloads](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#deployment-status) +and the `Ready` status of custom resources. + +A health check entry can reference one of the following types: + +- Kubernetes built-in kinds: Deployment, DaemonSet, StatefulSet, + PersistentVolumeClaim, Pod, PodDisruptionBudget, Job, CronJob, Service, + Secret, ConfigMap, CustomResourceDefinition +- Flux kinds: HelmRelease, HelmRepository, GitRepository, etc. +- Custom resources that are compatible with [kstatus](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus) + +Assuming the Kustomization source contains a Kubernetes Deployment named +`backend`, a health check can be defined as follows: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: backend + namespace: default +spec: + interval: 5m + prune: true + sourceRef: + kind: GitRepository + name: webapp + healthChecks: + - apiVersion: apps/v1 + kind: Deployment + name: backend + namespace: dev +``` + +After applying the kustomize build output, the controller verifies if the +rollout was completed successfully. If the deployment was successful, the +Kustomization `Ready` condition is marked as `True`, if the rollout failed, +or if it takes more than the specified timeout to complete, then the +Kustomization `Ready` condition is set to `False`. If the deployment becomes +healthy on the next execution, then the Kustomization is marked as ready. + +When a Kustomization contains HelmRelease objects, instead of checking the +underlying Deployments, you can define a health check that waits for the +HelmReleases to be reconciled with: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: webapp + namespace: default +spec: + interval: 15m + path: "./releases/" + prune: true + sourceRef: + kind: GitRepository + name: webapp + healthChecks: + - apiVersion: helm.toolkit.fluxcd.io/v2 + kind: HelmRelease + name: frontend + namespace: dev + - apiVersion: helm.toolkit.fluxcd.io/v2 + kind: HelmRelease + name: backend + namespace: dev + timeout: 5m +``` + +If all the HelmRelease objects are successfully installed or upgraded, then +the Kustomization will be marked as ready. + +### Health check expressions + +`.spec.healthCheckExprs` can be used to define custom logic for performing +health checks on custom resources. This is done through Common Expression +Language (CEL) expressions. This field accepts a list of objects with the +following fields: + +- `apiVersion`: The API version of the custom resource. Required. +- `kind`: The kind of the custom resource. Required. +- `current`: A required CEL expression that returns `true` if the resource is ready. +- `inProgress`: An optional CEL expression that returns `true` if the resource + is still being reconciled. +- `failed`: An optional CEL expression that returns `true` if the resource + failed to reconcile. + +The controller will evaluate the expressions in the following order: + +1. `inProgress` if specified +2. `failed` if specified +3. `current` + +The first expression that evaluates to `true` will determine the health +status of the custom resource. + +For example, to define a set of health check expressions for the `SealedSecret` +custom resource: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: sealed-secrets + namespace: flux-system +spec: + interval: 5m + path: ./path/to/sealed/secrets + prune: true + sourceRef: + kind: GitRepository + name: flux-system + timeout: 1m + wait: true # Tells the controller to wait for all resources to be ready by performing health checks. + healthCheckExprs: + - apiVersion: bitnami.com/v1alpha1 + kind: SealedSecret + failed: status.conditions.filter(e, e.type == 'Synced').all(e, e.status == 'False') + current: status.conditions.filter(e, e.type == 'Synced').all(e, e.status == 'True') +``` + +A common error is writing expressions that reference fields that do not +exist in the custom resource. This will cause the controller to wait +for the resource to be ready until the timeout is reached. To avoid this, +make sure your CEL expressions are correct. The +[CEL Playground](https://playcel.undistro.io/) is a useful resource for +this task. The input passed to each expression is the custom resource +object itself. You can check for field existence with the +[`has(...)` CEL macro](https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros), +just be aware that `has(status)` errors if `status` does not (yet) exist +on the top level of the resource you are using. + +It's worth checking if [the library](/flux/cheatsheets/cel-healthchecks/) +has expressions for the custom resources you are using. + +### Wait + +`.spec.wait` is an optional boolean field to perform health checks for __all__ +reconciled resources as part of the Kustomization. If set to `true`, +`.spec.healthChecks` is ignored. + +### Timeout + +`.spec.timeout` is an optional field to specify a timeout duration for any +operation like building, applying, health checking, etc. performed during the +reconciliation process. + +### Dependencies + +`.spec.dependsOn` is an optional list used to refer to other Kustomization +objects that the Kustomization depends on. If specified, then the Kustomization +is only applied after the referred Kustomizations are ready, i.e. have the +`Ready` condition marked as `True`. The readiness state of a Kustomization is +determined by its last applied status condition. + +This is helpful when there is a need to make sure other resources exist before +the workloads defined in a Kustomization are deployed. For example, before +installing objects of a certain custom resource kind, the CRDs and the related +controller must exist in the cluster. + +For example, assuming we have two Kustomizations: + +- cert-manager: reconciles the cert-manager CRDs and controller +- certs: reconciles the cert-manager custom resources + +You can instruct the controller to apply the `cert-manager` Kustomization before +`certs` by defining a `dependsOn` relationship between the two: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cert-manager + namespace: flux-system +spec: + interval: 5m + path: "./cert-manager/controller" + prune: true + sourceRef: + kind: GitRepository + name: flux-system + healthChecks: + - apiVersion: apps/v1 + kind: Deployment + name: cert-manager + namespace: cert-manager +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: certs + namespace: flux-system +spec: + dependsOn: + - name: cert-manager + interval: 5m + path: "./cert-manager/certs" + prune: true + sourceRef: + kind: GitRepository + name: flux-system +``` + +If `.spec.healthChecks` is non-empty or `.spec.wait` is set to `true`, a +Kustomization will be applied after all its dependencies' health checks have +passed. For example, this can be used to ensure a service mesh proxy injector +is running before deploying applications inside the mesh. + +**Note:** Circular dependencies between Kustomizations must be avoided, +otherwise the interdependent Kustomizations will never be applied on the cluster. + +### Service Account reference + +`.spec.serviceAccountName` is an optional field used to specify the +ServiceAccount to be impersonated while reconciling the Kustomization. For more +details, see [Role-based Access Control](#role-based-access-control). + +### Common metadata + +`.spec.commonMetadata` is an optional field used to specify any metadata that +should be applied to all the Kustomization's resources. It has two optional fields: + +- `labels`: A map used for setting [labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) + on an object. Any existing label will be overridden if it matches with a key in + this map. +- `annotations`: A map used for setting [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) + on an object. Any existing annotation will be overridden if it matches with a key + in this map. + +### Name Prefix and Suffix + +`.spec.namePrefix` and `.spec.nameSuffix` are optional fields used to specify a prefix and suffix +to be added to the names of all the resources in the Kustomization. + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app +spec: + # ...omitted for brevity + namePrefix: "prefix-" + nameSuffix: "-suffix" +``` + +### Patches + +`.spec.patches` is an optional list used to specify [Kustomize `patches`](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patches/) +as inline YAML objects. This enables patching resources using either a +[strategic merge](https://kubectl.docs.kubernetes.io/references/kustomize/glossary#patchstrategicmerge) +patch or a [JSON6902](https://kubectl.docs.kubernetes.io/references/kustomize/glossary#patchjson6902) +patch. A patch can target a single resource or multiple resources. Each item in +the list must have the two fields mentioned below: + +- `patch`: Patch contains an inline strategic merge patch or an inline JSON6902 patch with an array of operation objects. +- `target`: Target points to the resources that the patch document should be applied to. + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: flux-system +spec: + # ...omitted for brevity + patches: + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: not-used + spec: + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: "true" + target: + kind: Deployment + labelSelector: "app.kubernetes.io/part-of=my-app" + - patch: | + - op: add + path: /spec/template/spec/securityContext + value: + runAsUser: 10000 + fsGroup: 1337 + - op: add + path: /spec/template/spec/containers/0/securityContext + value: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + target: + kind: Deployment + name: podinfo + namespace: apps +``` + +### Images + +`.spec.images` is an optional list used to specify +[Kustomize `images`](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/images/). +This allows overwriting the name, tag or digest of container images without creating patches. + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: flux-system +spec: + # ...omitted for brevity + images: + - name: podinfo + newName: my-registry/podinfo + newTag: v1 + - name: podinfo + newTag: 1.8.0 + - name: podinfo + newName: my-podinfo + - name: podinfo + digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 +``` + +### Components + +`.spec.components` is an optional list used to specify +[Kustomize `components`](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/components/). +This allows using reusable pieces of configuration logic that can be included +from multiple overlays. + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: flux-system +spec: + # ...omitted for brevity + components: + - ../ingress + - ../tls +``` + +**Note:** The components paths must be local and relative to the path specified by `.spec.path`. + +**Warning:** Components are an alpha feature in Kustomize and are therefore +considered experimental in Flux. No guarantees are provided as the feature may +be modified in backwards incompatible ways or removed without warning. + +### Post build variable substitution + +With `.spec.postBuild.substitute` you can provide a map of key-value pairs +holding the variables to be substituted in the final YAML manifest, after +kustomize build. + +With `.spec.postBuild.substituteFrom` you can provide a list of ConfigMaps and +Secrets from which the variables are loaded. The ConfigMap and Secret data keys +are used as the variable names. + +The `.spec.postBuild.substituteFrom.optional` field indicates how the +controller should handle a referenced ConfigMap or Secret being absent +at reconciliation time. The controller's default behavior ― with +`optional` unspecified or set to `false` ― it has failed reconciliation if +the referenced object is missing. By setting the `optional` field to +`true`, you can indicate that the controller should use the referenced +object if it's there, but also tolerate its absence, treating that +absence as if the object had been present but empty, defining no +variables. + +This offers basic templating for your manifests including support +for [bash string replacement functions](https://github.com/fluxcd/pkg/blob/main/envsubst/README.md) e.g.: + +- `${var:=default}` +- `${var:position}` +- `${var:position:length}` +- `${var/substring/replacement}` + +**Note:** The name of a variable can contain only alphanumeric and underscore +characters. The controller validates the variable names using this regular +expression: `^[_[:alpha:]][_[:alpha:][:digit:]]*$`. + +For example, assuming we have manifests with the following variables: + +```yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: apps + labels: + environment: ${cluster_env:=dev} + region: "${cluster_region}" +``` + +You can specify the variables and their values in the Kustomization definition using +`.spec.postBuild.substitute` and/or `.spec.postBuild.substituteFrom`: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: apps +spec: + # ...omitted for brevity + postBuild: + substitute: + cluster_env: "prod" + cluster_region: "eu-central-1" + substituteFrom: + - kind: ConfigMap + name: cluster-vars + # Use this ConfigMap if it exists, but proceed if it doesn't. + optional: true + - kind: Secret + name: cluster-secret-vars + # Fail if this Secret does not exist. +``` + +**Note:** For substituting variables in a secret, `.spec.stringData` field must be used i.e: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret + namespace: flux-system +type: Opaque +stringData: + token: ${token} +``` + +**Note:** The var values which are specified in-line with `substitute` +take precedence over the ones derived from `substituteFrom`. +When var values for the same variable keys are derived from multiple +`ConfigMaps` or `Secrets` referenced in the `substituteFrom` list, then the +first take precedence over the later values. + +**Note:** If you want to avoid var substitutions in scripts embedded in +ConfigMaps or container commands, you must use the format `$var` instead of +`${var}`. If you want to keep the curly braces you can use `$${var}` which +will print out `${var}`. + +All the undefined variables in the format `${var}` will be substituted with an +empty string unless a default value is provided e.g. `${var:=default}`. + +**Note:** It is recommended to set the `--feature-gates=StrictPostBuildSubstitutions=true` +controller flag, so that the post-build substitutions will fail if a +variable without a default value is declared in files but is +missing from the input vars. + +You can disable the variable substitution for certain resources by either +labelling or annotating them with: + +```yaml +kustomize.toolkit.fluxcd.io/substitute: disabled +``` + +Substitution of variables only happens if at least a single variable or resource +to substitute from is defined. This may cause issues if you rely on expressions +which should evaluate to a default value, even if no other variables are +configured. To work around this, one can set an arbitrary key/value pair to +enable the substitution of variables. For example: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: apps +spec: + # ...omitted for brevity + postBuild: + substitute: + var_substitution_enabled: "true" +``` + +**Note:** When using numbers or booleans as values for variables, they must be +enclosed in double quotes vars to be treated as strings, for more information see +[substitution of numbers and booleans](#post-build-substitution-of-numbers-and-booleans). + +You can replicate the controller post-build substitutions locally using +[kustomize](https://github.com/kubernetes-sigs/kustomize) +and the Flux CLI: + +```console +$ export cluster_region=eu-central-1 +$ kustomize build ./apps/ | flux envsubst --strict +--- +apiVersion: v1 +kind: Namespace +metadata: + name: apps + labels: + environment: dev + region: eu-central-1 +``` + +### Force + +`.spec.force` is an optional boolean field. If set to `true`, the controller +will replace the resources in-cluster if the patching fails due to immutable +field changes. + +It can also be enabled for specific resources by labelling or annotating them +with: + +```yaml +kustomize.toolkit.fluxcd.io/force: enabled +``` + +### KubeConfig reference + +`.spec.kubeConfig.secretRef.Name` is an optional field to specify the name of +the secret containing a KubeConfig. If specified, objects will be applied, +health-checked, pruned, and deleted for the default cluster specified in that +KubeConfig instead of using the in-cluster ServiceAccount. + +The secret defined in the `kubeConfig.SecretRef` must exist in the same +namespace as the Kustomization. On every reconciliation, the KubeConfig bytes +will be loaded from the `.secretRef.key` key (default: `value` or `value.yaml`) +of the Secret’s data , and the Secret can thus be regularly updated if +cluster-access-tokens have to rotate due to expiration. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: prod-kubeconfig +type: Opaque +stringData: + value.yaml: | + apiVersion: v1 + kind: Config + # ...omitted for brevity +``` + +**Note:** The KubeConfig should be self-contained and not rely on binaries, +environment, or credential files from the kustomize-controller Pod. +This matches the constraints of KubeConfigs from current Cluster API providers. +KubeConfigs with `cmd-path` in them likely won't work without a custom, +per-provider installation of kustomize-controller. + +When both `.spec.kubeConfig` and `.spec.ServiceAccountName` are specified, +the controller will impersonate the service account on the target cluster. + +For more information, see [remote clusters/Cluster-API](#remote-clusterscluster-api). + +### Decryption + +Storing Secrets in Git repositories in plain text or base64 is unsafe, +regardless of the visibility or access restrictions of the repository. + +In order to store Secrets safely in Git repositorioes you can use an +encryption provider and the optional field `.spec.decryption` to +configure decryption for Secrets that are a part of the Kustomization. + +The only supported encryption provider is [SOPS](https://getsops.io/). +With SOPS you can encrypt your secrets with [age](https://github.com/FiloSottile/age) +or [OpenPGP](https://www.openpgp.org) keys, or with keys from Key Management Services +(KMS), like AWS KMS, Azure Key Vault, GCP KMS or Hashicorp Vault. + +**Note:** You must leave `metadata`, `kind` or `apiVersion` in plain text. +An easy way to do this is limiting the encrypted keys with the flag +`--encrypted-regex '^(data|stringData)$'` in your `sops encrypt` command. + +The `.spec.decryption` field has the following subfields: + +- `.provider`: The secrets decryption provider to be used. This field is required and + the only supported value is `sops`. +- `.secretRef.name`: The name of the secret that contains the keys or cloud provider + static credentials for KMS services to be used for decryption. +- `.serviceAccountName`: The name of the service account used for + secret-less authentication with KMS services from cloud providers. + +For a complete guide on how to set up authentication for KMS services from +cloud providers, see the integration [docs](/flux/integrations/). + +If a static credential for a given cloud provider is defined inside the secret +referenced by `.secretRef`, that static credential takes priority over secret-less +authentication for that provider. If no static credentials are defined for a given +cloud provider inside the secret, secret-less authentication is attempted for that +provider. + +If `.serviceAccountName` is specified for secret-less authentication, +it takes priority over [controller global decryption](#controller-global-decryption) +for all cloud providers. + +Example: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: sops-encrypted + namespace: default +spec: + interval: 5m + path: "./" + sourceRef: + kind: GitRepository + name: repository-with-secrets + decryption: + provider: sops + serviceAccountName: sops-identity + secretRef: + name: sops-keys-and-credentials +``` + +The Secret's `.data` section is expected to contain entries with decryption +keys (for age and OpenPGP), or credentials (for any of the supported provider +implementations). The controller identifies the type of the entry by the suffix +of the key (e.g. `.agekey`), or a fixed key (e.g. `sops.vault-token`). + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys-and-credentials + namespace: default +data: + # Exemplary age private key + identity.agekey: + # Exemplary Hashicorp Vault token + sops.vault-token: +``` + +#### age Secret entry + +To specify an age private key in a Kubernetes Secret, suffix the key of the +`.data` entry with `.agekey`. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary age private key + identity.agekey: +``` + +#### OpenPGP Secret entry + +To specify an OpenPGP (passwordless) keyring in armor format in a Kubernetes +Secret, suffix the key of the `.data` entry with `.asc`. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary OpenPGP keyring + identity.asc: +``` + +#### AWS KMS Secret entry + +To specify credentials for an AWS user account linked to the IAM role with access +to KMS, append a `.data` entry with a fixed `sops.aws-kms` key. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + sops.aws-kms: | + aws_access_key_id: some-access-key-id + aws_secret_access_key: some-aws-secret-access-key + aws_session_token: some-aws-session-token # this field is optional +``` + +#### Azure Key Vault Secret entry + +To specify credentials for Azure Key Vault in a Secret, append a `.data` entry +with a fixed `sops.azure-kv` key. The value can contain a variety of JSON or +YAML formats depending on the authentication method you want to utilize. + +##### Service Principal with Secret + +To configure a Service Principal with Secret credentials to access the Azure +Key Vault, a JSON or YAML object with `tenantId`, `clientId` and `clientSecret` +fields must be configured as the `sops.azure-kv` value. It optionally supports +`authorityHost` to configure an authority host other than the Azure Public Cloud +endpoint. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Service Principal with Secret + sops.azure-kv: | + tenantId: some-tenant-id + clientId: some-client-id + clientSecret: some-client-secret +``` + +##### Service Principal with Certificate + +To configure a Service Principal with Certificate credentials to access the +Azure Key Vault, a JSON or YAML object with `tenantId`, `clientId` and +`clientCertificate` fields must be configured as the `sops.azure-kv` value. +It optionally supports `clientCertificateSendChain` and `authorityHost` to +control the sending of the certificate chain, or to specify an authority host +other than the Azure Public Cloud endpoint. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Service Principal with Certificate + sops.azure-kv: | + tenantId: some-tenant-id + clientId: some-client-id + clientCertificate: +``` + +##### `az` generated Service Principal + +To configure a Service Principal [generated using +`az`](https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal), +the output of the command can be directly used as a `sops.azure-kv` value. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Service Principal generated with `az` + sops.azure-kv: | + { + "appId": "559513bd-0c19-4c1a-87cd-851a26afd5fc", + "displayName": "myAKSClusterServicePrincipal", + "name": "http://myAKSClusterServicePrincipal", + "password": "e763725a-5eee-40e8-a466-dc88d980f415", + "tenant": "72f988bf-86f1-41af-91ab-2d7cd011db48" + } +``` + +##### Managed Identity with Client ID + +To configure a Managed Identity making use of a Client ID, a JSON or YAML +object with a `clientId` must be configured as the `sops.azure-kv` value. It +optionally supports `authorityHost` to configure an authority host other than +the Azure Public Cloud endpoint. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Managed Identity with Client ID + sops.azure-kv: | + clientId: some-client-id +``` + +#### GCP KMS Secret entry + +To specify credentials for GCP KMS in a Kubernetes Secret, append a `.data` +entry with a fixed `sops.gcp-kms` key and the service account keys as its value. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary GCP Service Account credentials file + sops.gcp-kms: | + { + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "" + } +``` + +#### Hashicorp Vault Secret entry + +To specify credentials for Hashicorp Vault in a Kubernetes Secret, append a +`.data` entry with a fixed `sops.vault-token` key and the token as value. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary Hashicorp Vault Secret token + sops.vault-token: +``` + +## Working with Kustomizations + +### Recommended settings + +When deploying applications to production environments, it is recommended +to configure the following fields, while adjusting them to your desires for +responsiveness: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: webapp + namespace: apps +spec: + interval: 1m0s # check for new commits every minute and apply changes + url: https://github.com/org/webapp # clone over HTTPS + secretRef: # use token auth + name: webapp-git-token # Flux user PAT (read-only access) + ref: + branch: main + ignore: | + # exclude all + /* + # include deploy dir + !/deploy +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: webapp + namespace: apps +spec: + interval: 60m0s # detect drift and undo kubectl edits every hour + wait: true # wait for all applied resources to become ready + timeout: 3m0s # give up waiting after three minutes + retryInterval: 2m0s # retry every two minutes on apply or waiting failures + prune: true # remove stale resources from cluster + force: false # enable this to recreate resources on immutable fields changes + targetNamespace: apps # set the namespace for all resources + sourceRef: + kind: GitRepository + name: webapp + namespace: apps + path: "./deploy/production" +``` + +### Generating a `kustomization.yaml` file + +If your repository contains plain Kubernetes manifests without a +`kustomization.yaml`, the file is automatically generated for all the +Kubernetes manifests in the directory tree specified in [`.spec.path`](#path). + +All YAML files present under that path must be valid Kubernetes manifests, +unless they're excluded either by way of the [`.sourceignore`](https://fluxcd.io/flux/components/source/gitrepositories/#sourceignore-file) +file or the [`.spec.ignore`](https://fluxcd.io/flux/components/source/gitrepositories/#ignore) +field on the corresponding Source object. + +Example of excluding CI workflows and SOPS config files: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: default +spec: + interval: 5m + url: https://github.com/stefanprodan/podinfo + ignore: | + .github/ + .sops.yaml + .gitlab-ci.yml +``` + +It is recommended to generate the `kustomization.yaml` on your own and store it +in Git, this way you can validate your manifests in CI +([example script](https://github.com/fluxcd/flux2-multi-tenancy/blob/main/scripts/validate.sh)). +Assuming your manifests are inside `apps/my-app`, you can generate a +`kustomization.yaml` with: + +```sh +cd apps/my-app + +# create kustomization.yaml +kustomize create --autodetect --recursive +``` + +### Controlling the apply behavior of resources + +To change the apply behaviour for specific Kubernetes resources, you can annotate them with: + +| Annotation | Default | Values | Role | +|-------------------------------------|------------|----------------------------------------------------------------|-----------------| +| `kustomize.toolkit.fluxcd.io/ssa` | `Override` | - `Override`
- `Merge`
- `IfNotPresent`
- `Ignore` | Apply policy | +| `kustomize.toolkit.fluxcd.io/force` | `Disabled` | - `Enabled`
- `Disabled` | Recreate policy | +| `kustomize.toolkit.fluxcd.io/prune` | `Enabled` | - `Enabled`
- `Disabled` | Delete policy | + +**Note:** These annotations should be set in the Kubernetes YAML manifests included +in the Flux Kustomization source (Git, OCI, Bucket). + +#### `kustomize.toolkit.fluxcd.io/ssa` + +##### Override + +The `Override` policy instructs the controller to reconcile the Kubernetes resources +with the desired state (YAML manifests) defined in the Flux source (Git, OCI, Bucket). + +If you use `kubectl` to edit a Kubernetes resource managed by Flux, all changes will be +reverted when the controller reconciles a Flux Kustomization containing that resource. +In order to preserve fields added with `kubectl`, you have to specify +a field manager named `flux-client-side-apply` e.g.: + +```sh +kubectl apply --field-manager=flux-client-side-apply +``` + +##### Merge + +The `Merge` policy instructs the controller to preserve the fields added by other tools to the +Kubernetes resources managed by Flux. + +The fields defined in the manifests applied by the controller will always be overridden, +the `Merge` policy works only for adding new fields that don’t overlap with the desired +state. + +For lists fields which are atomic (e.g. `.spec.tolerations` in PodSpec), Kubernetes +doesn't allow different managers for such fields, therefore any changes to these +fields will be reverted. For more context, please see the Kubernetes enhancement document: +[555-server-side-apply](https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/555-server-side-apply/README.md#lists). + +##### IfNotPresent + +The `IfNotPresent` policy instructs the controller to only apply the Kubernetes resources +if they are not present on the cluster. + +This policy can be used for Kubernetes Secrets and ValidatingWebhookConfigurations managed by cert-manager, +where Flux creates the resources with fields that are later on mutated by other controllers. + +##### Ignore + +The `Ignore` policy instructs the controller to skip applying Kubernetes resources +even if they are included in a Flux source (Git, OCI, Bucket). + +#### `kustomize.toolkit.fluxcd.io/force` + +When set to `Enabled`, this policy instructs the controller to recreate the Kubernetes resources +with changes to immutable fields. + +This policy can be used for Kubernetes Jobs to rerun them when their container image changes. + +**Note:** Using this policy for StatefulSets may result in potential data loss. + +#### `kustomize.toolkit.fluxcd.io/prune` + +When set to `Disabled`, this policy instructs the controller to skip the deletion of +the Kubernetes resources subject to [garbage collection](#prune). + +This policy can be used to protect sensitive resources such as Namespaces, PVCs and PVs +from accidental deletion. + +### Role-based access control + +By default, a Kustomization apply runs under the cluster admin account and can +create, modify and delete cluster level objects (namespaces, CRDs, etc) and +namespaced objects (deployments, ingresses, etc). For certain Kustomizations a +cluster admin may wish to control what types of Kubernetes objects can be +reconciled and under which namespaces. +To restrict a Kustomization, one can assign a service account under which the +reconciliation is performed using [`.spec.serviceAccountName`](#service-account-reference). + +Assuming you want to restrict a group of Kustomizations to a single namespace, +you can create an account with a role binding that grants access only to that namespace: + +```yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: webapp +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flux + namespace: webapp +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: webapp-reconciler + namespace: webapp +rules: + - apiGroups: ['*'] + resources: ['*'] + verbs: ['*'] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: webapp-reconciler + namespace: webapp +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: webapp-reconciler +subjects: +- kind: ServiceAccount + name: flux + namespace: webapp +``` + +**Note:** The namespace, RBAC and service account manifests should be +placed in a Git source and applied with a Kustomization. The Kustomizations that +are running under that service account should depend on the one that contains the account. + +Create a Kustomization that prevents altering the cluster state outside the +`webapp` namespace: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: backend + namespace: webapp +spec: + serviceAccountName: flux + dependsOn: + - name: rbac + interval: 5m + path: "./webapp/backend/" + prune: true + sourceRef: + kind: GitRepository + name: webapp +``` + +When the controller reconciles the `backend` Kustomization, it will impersonate +the `flux` ServiceAccount. If the Kustomization contains cluster level objects +like CRDs or objects belonging to a different namespace, the reconciliation will +fail since the account it runs under has no permissions to alter objects outside +the `webapp` namespace. + +#### Enforcing impersonation + +On multi-tenant clusters, platform admins can enforce impersonation with the +`--default-service-account` flag. + +When the flag is set, all Kustomizations which don't have [`.spec.serviceAccountName`](#service-account-reference) +specified will use the service account name provided by +`--default-service-account=` in the namespace of the object. + +### Remote clusters/Cluster-API + +With the [`.spec.kubeConfig` field](#kubeconfig-reference) a Kustomization can be fully +reconciled on a remote cluster. This composes well with Cluster API bootstrap +providers such as CAPBK (kubeadm), CAPA (AWS) and others. + +To reconcile a Kustomization to a CAPI controlled cluster, put the +`Kustomization` in the same namespace as your `Cluster` object, and set the +`kubeConfig.secretRef.name` to `-kubeconfig`: + +```yaml +apiVersion: cluster.x-k8s.io/v1alpha3 +kind: Cluster +metadata: + name: stage # the kubeconfig Secret will contain the Cluster name + namespace: capi-stage +spec: + clusterNetwork: + pods: + cidrBlocks: + - 10.100.0.0/16 + serviceDomain: stage-cluster.local + services: + cidrBlocks: + - 10.200.0.0/12 + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1alpha3 + kind: KubeadmControlPlane + name: stage-control-plane + namespace: capi-stage + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha3 + kind: DockerCluster + name: stage + namespace: capi-stage +--- +# ... unrelated Cluster API objects omitted for brevity ... +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cluster-addons + namespace: capi-stage +spec: + interval: 5m + path: "./config/addons/" + prune: true + sourceRef: + kind: GitRepository + name: cluster-addons + kubeConfig: + secretRef: + name: stage-kubeconfig # Cluster API creates this for the matching Cluster +``` + +The Cluster and Kustomization can be created at the same time. +The Kustomization will eventually reconcile once the cluster is available. + +If you wish to target clusters created by other means than CAPI, you can create +a ServiceAccount on the remote cluster, generate a KubeConfig for that account +and then create a secret on the cluster where kustomize-controller is running. +For example: + +```sh +kubectl create secret generic prod-kubeconfig \ + --from-file=value.yaml=./kubeconfig +``` + +### Controller global decryption + +Other than [authentication using a Secret reference](#decryption), +it is possible to specify global decryption settings on the +kustomize-controller Pod. When the controller fails to find credentials on the +Kustomization object itself, it will fall back to these defaults. + +See also the [workload identity](/flux/installation/configuration/workload-identity/) docs. + +#### AWS KMS + +While making use of the [IAM OIDC provider](https://eksctl.io/usage/iamserviceaccounts/) +on your EKS cluster, you can create an IAM Role and Service Account with access +to AWS KMS (using at least `kms:Decrypt` and `kms:DescribeKey`). Once these are +created, you can annotate the kustomize-controller Service Account with the +Role ARN, granting the controller permission to decrypt the Secrets. Please refer +to the [SOPS guide](https://fluxcd.io/flux/guides/mozilla-sops/#aws) for detailed steps. + +```sh +kubectl -n flux-system annotate serviceaccount kustomize-controller \ + --field-manager=flux-client-side-apply \ + eks.amazonaws.com/role-arn='arn:aws:iam:::role/' +``` + +Furthermore, you can also use the usual [environment variables used for specifying AWS +credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html#envvars-list), +by patching the kustomize-controller Deployment: + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kustomize-controller + namespace: flux-system +spec: + template: + spec: + containers: + - name: manager + env: + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: aws-creds + key: awsAccessKeyID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: aws-creds + key: awsSecretAccessKey + - name: AWS_SESSION_TOKEN + valueFrom: + secretKeyRef: + name: aws-creds + key: awsSessionToken +``` + +In addition to this, the +[general SOPS documentation around KMS AWS applies](https://github.com/mozilla/sops#27kms-aws-profiles), +allowing you to specify e.g. a `SOPS_KMS_ARN` environment variable. + +**Note:**: If you are mounting a secret containing the AWS credentials as a +file in the `kustomize-controller` Pod, you need to specify an environment +variable `$HOME`, since the AWS credentials file is expected to be present at +`~/.aws`. For example: + +```yaml +env: + - name: HOME + value: /home/{$USER} +``` + +#### Azure Key Vault + +##### Workload Identity + +If you have Workload Identity set up on your AKS cluster, you can establish +a federated identity between the kustomize-controller ServiceAccount and an +identity that has "Decrypt" role on the Azure Key Vault. Once, this is done +you can label and annotate the kustomize-controller ServiceAccount and Pod +with the patch shown below: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: |- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: kustomize-controller + namespace: flux-system + annotations: + azure.workload.identity/client-id: + labels: + azure.workload.identity/use: "true" + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: kustomize-controller + namespace: flux-system + labels: + azure.workload.identity/use: "true" + spec: + template: + metadata: + labels: + azure.workload.identity/use: "true" +``` + +##### Kubelet Identity + +If the kubelet managed identity has `Decrypt` permissions on Azure Key Vault, +no additional configuration is required for the kustomize-controller to decrypt +data. + +#### GCP KMS + +While making use of Google Cloud Platform, the [`GOOGLE_APPLICATION_CREDENTIALS` +environment variable](https://cloud.google.com/docs/authentication/production) +is automatically taken into account. +[Granting permissions](https://cloud.google.com/kms/docs/reference/permissions-and-roles) +to the Service Account attached to this will therefore be sufficient to decrypt +data. When running outside GCP, it is possible to manually patch the +kustomize-controller Deployment with a valid set of (mounted) credentials. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kustomize-controller + namespace: flux-system +spec: + template: + spec: + containers: + - name: manager + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /var/gcp/credentials.json + volumeMounts: + - name: gcp-credentials + mountPath: /var/gcp/ + readOnly: true + volumes: + - name: gcp-credentials + secret: + secretName: mysecret + items: + - key: credentials + path: credentials.json +``` + +#### Hashicorp Vault + +To configure a global default for Hashicorp Vault, patch the controller's +Deployment with a `VAULT_TOKEN` environment variable. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kustomize-controller + namespace: flux-system +spec: + template: + spec: + containers: + - name: manager + env: + - name: VAULT_TOKEN + value: +``` + +### Kustomize secretGenerator + +SOPS encrypted data can be stored as a base64 encoded Secret, which enables the +use of [Kustomize `secretGenerator`](https://github.com/kubernetes-sigs/kustomize/tree/main/examples/secretGeneratorPlugin.md) +as follows: + +```console +$ echo "my-secret-token" | sops -e /dev/stdin > token.encrypted +$ cat < kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +secretGenerator: + - name: token + files: + - token=token.encrypted +EOF +``` + +Commit and push `token.encrypted` and `kustomization.yaml` to Git. + +The kustomize-controller scans the values of Kubernetes Secrets, and when it +detects that the values are SOPS encrypted, it decrypts them before applying +them on the cluster. + +For secrets in `.json`, `.yaml` `.ini` and `.env` format, make sure you specify +the input type when encrypting them with SOPS: + +```sh +sops -e --input-type=json config.json > config.json.encrypted +sops -e --input-type=yaml config.yaml > config.yaml.encrypted +sops -e --input-type=env config.env > config.env.encrypted +``` + +For kustomize-controller to be able to decrypt a JSON config, you need to set +the file extension to `.json`: + +```yaml +kind: Kustomization +secretGenerator: + - name: config + files: + - config.json=config.json.encrypted +``` + +For dotenv files, use the `envs` directive: + +```yaml +kind: Kustomization +secretGenerator: + - name: config + envs: + - config.env.encrypted +``` + +For Docker config files, you need to specify both input and output type as JSON: + +```sh +sops -e --input-type=json --output-type=json ghcr.dockerconfigjson > ghcr.dockerconfigjson.encrypted +``` + +To generate an image pull secret, use the `.dockerconfigjson` as the secret key: + +```yaml +kind: Kustomization +secretGenerator: + - name: ghcr-auth + type: kubernetes.io/dockerconfigjson + files: + - .dockerconfigjson=ghcr.dockerconfigjson.encrypted +``` + +### Post build substitution of numbers and booleans + +When using [variable substitution](#post-build-variable-substitution) with values +that are numbers or booleans, the reconciliation may fail if the substitution +is for a field that must be of type string. To convert the number or boolean +to a string, you can wrap the variable with a double quotes var: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: app + annotations: + id: ${quote}${id}${quote} + enabled: ${quote}${enabled}${quote} +``` + +Then in the Flux Kustomization, define the variables as: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app +spec: + postBuild: + substitute: + quote: '"' # double quote var + id: "123" + enabled: "true" +``` + +### Triggering a reconcile + +To manually tell the kustomize-controller to reconcile a Kustomization outside +the [specified interval window](#interval), it can be annotated with +`reconcile.fluxcd.io/requestedAt: `. Annotating the resource +queues the Kustomization for reconciliation if the `` differs +from the last value the controller acted on, as reported in +[`.status.lastHandledReconcileAt`](#last-handled-reconcile-at). + +Using `kubectl`: + +```sh +kubectl annotate --field-manager=flux-client-side-apply --overwrite kustomization/ reconcile.fluxcd.io/requestedAt="$(date +%s)" +``` + +Using `flux`: + +```sh +flux reconcile kustomization +``` + +### Waiting for `Ready` + +When a change is applied, it is possible to wait for the Kustomization to reach +a `Ready` state using `kubectl`: + +```sh +kubectl wait kustomization/ --for=condition=ready --timeout=1m +``` + +### Suspending and resuming + +When you find yourself in a situation where you temporarily want to pause the +reconciliation of a Kustomization, you can suspend it using [`.spec.suspend`](#suspend). + +To pause the reconciliation of a specific Kubernetes resource managed by a Flux Kustomization, +you can annotate or label the resource in-cluster with: + +```yaml +kustomize.toolkit.fluxcd.io/reconcile: disabled +``` + +**Note:** When the `kustomize.toolkit.fluxcd.io/reconcile` annotation is set to +`disabled`, the controller will no longer apply changes, nor +will it prune the resource. To resume reconciliation, set the annotation to +`enabled` in the source or remove it from the in-cluster object. + +#### Suspend a Kustomization + +In your YAML declaration: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: +spec: + suspend: true +``` + +Using `kubectl`: + +```sh +kubectl patch kustomization --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}' +``` + +Using `flux`: + +```sh +flux suspend kustomization +``` + +#### Resume a Kustomization + +In your YAML declaration, comment out (or remove) the field: + +```yaml +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: +spec: + # suspend: true +``` + +**Note:** Setting the field value to `false` has the same effect as removing +it, but does not allow for "hot patching" using e.g. `kubectl` while practicing +GitOps; as the manually applied patch would be overwritten by the declared +state in Git. + +Using `kubectl`: + +```sh +kubectl patch kustomization --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}' +``` + +Using `flux`: + +```sh +flux resume kustomization +``` + +### Debugging a Kustomization + +There are several ways to gather information about a Kustomization for +debugging purposes. + +#### Describe the Kustomization + +Describing a Kustomization using +`kubectl describe kustomization ` +displays the latest recorded information for the resource in the `Status` and +`Events` sections: + +```console +... +Status: +... + Conditions: + Last Transition Time: 2023-03-29T06:09:32Z + Message: Fetching manifests for revision master/67e2c98a60dc92283531412a9e604dd4bae005a9 with a timeout of 4m30s + Observed Generation: 3 + Reason: ProgressingWithRetry + Status: True + Type: Reconciling + Last Transition Time: 2023-03-29T06:09:32Z + Message: kustomization path not found: stat /tmp/kustomization-1464362706/invalid: no such file or directory + Observed Generation: 3 + Reason: ArtifactFailed + Status: False + Type: Ready + Last Applied Revision: master/67e2c98a60dc92283531412a9e604dd4bae005a9 + Last Attempted Revision: master/67e2c98a60dc92283531412a9e604dd4bae005a9 + Observed Generation: 2 +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning ArtifactFailed 2s kustomize-controller kustomization path not found: stat /tmp/kustomization-1464362706/invalid: no such file or directory + +``` + +#### Trace emitted Events + +To view events for specific Kustomization(s), `kubectl events` can be used +to list the Events for specific objects. For example, running + +```sh +kubectl events -n flux-system --for kustomization/podinfo +``` + +lists + +```console +LAST SEEN TYPE REASON OBJECT MESSAGE +31s Warning ArtifactFailed kustomization/podinfo kustomization path not found: stat /tmp/kustomization-3011588360/invalid: no such file or directory +26s Normal ArtifactFailed kustomization/podinfo HorizontalPodAutoscaler/default/podinfo deleted... +18s Warning ArtifactFailed kustomization/podinfo kustomization path not found: stat /tmp/kustomization-3336282420/invalid: no such file or directory +9s Normal Progressing kustomization/podinfo Service/default/podinfo created... +9s Normal ReconciliationSucceeded kustomization/podinfo Reconciliation finished in 75.190237ms, next run in 5m0s +``` + +You can also use the `flux events` command to view all events for a +Kustomization and its related Source. For example, + +```sh +flux events --for Kustomization/podinfo +``` + +will list all events for the `podinfo` Kustomization in the `flux-system` +namespace and its related Source object, the `podinfo` GitRepository. + +```console +LAST SEEN TYPE REASON OBJECT MESSAGE +3m2s Warning ArtifactFailed Kustomization/podinfo kustomization path not found: stat /tmp/kustomization-3336282420/invalid: no such file or directory + +2m53s Normal ReconciliationSucceeded Kustomization/podinfo Reconciliation finished in 75.190237ms, next run in 5m0s + +2m53s Normal Progressing Kustomization/podinfo Service/default/podinfo created + Deployment/default/podinfo created + HorizontalPodAutoscaler/default/podinfo created + +19s (x17 over 8m24s) Normal GitOperationSucceeded GitRepository/podinfo no changes since last reconcilation: observed revision 'master/67e2c98a60dc92283531412a9e604dd4bae005a9' +``` + +Besides being reported in Events, the reconciliation errors are also logged by +the controller. The Flux CLI offer commands for filtering the logs for a +specific Kustomization, e.g. +`flux logs --level=error --kind=Kustomization --name=`. + +## Kustomization Status + +### Conditions + +A Kustomization enters various states during its lifecycle, reflected as +[Kubernetes Conditions][typical-status-properties]. +It can be [reconciling](#reconciling-kustomization) while applying the Kustomization on the cluster, it can be [ready](#ready-kustomization), or it can [fail during +reconciliation](#failed-kustomization). + +The Kustomization API is compatible with the [kstatus specification][kstatus-spec], +and reports `Reconciling` and `Stalled` conditions where applicable to +provide better (timeout) support to solutions polling the Kustomization to +become `Ready`. + +#### Reconciling Kustomization + +The kustomize-controller marks a Kustomization as _reconciling_ when it starts +the reconciliation of the same. The Condition added to the Kustomization's +`.status.conditions` has the following attributes: + +- `type: Reconciling` +- `status: "True"` +- `reason: Progressing` | `reason: ProgressingWithRetry` + +The Condition `message` is updated during the course of the reconciliation to +report the action being performed at any particular moment such as +building manifests, detecting drift, etc. + +The `Ready` Condition's `status` is also marked as `Unkown`. + +#### Ready Kustomization + +The kustomize-controller marks a Kustomization as _ready_ when a Kustomization +is successfully reconciled, i.e. the source was fetched, the kustomization was +built and applied on the cluster and all health checks are observed to be passing. + +When the Kustomization is "ready", the controller sets a Condition with the +following attributes in the Kustomization’s `.status.conditions`: + +- `type: Ready` +- `status: "True"` +- `reason: ReconciliationSucceeded` + +#### Failed Kustomization + +The kustomize-controller may get stuck trying to reconcile and apply a +Kustomization without completing. This can occur due to some of the following factors: + +- The Source object does not exist on the cluster. +- The Source has not produced an Artifact yet. +- The Kustomization's dependencies aren't ready yet. +- The specified path does not exist in the Artifact. +- Building the kustomization fails. +- Garbage collection fails. +- Running a health check failed. + +When this happens, the controller sets the `Ready` Condition status to False +and adds a Condition with the following attributes to the Kustomization’s +`.status.conditions`: + +- `type: Ready | HealthyCondition` +- `status: "False"` +- `reason: PruneFailed | ArtifactFailed | BuildFailed | HealthCheckFailed | DependencyNotReady | ReconciliationFailed ` + +The `message` field of the Condition will contain more information about why +the reconciliation failed. + +While the Kustomization has one or more of these Conditions, the controller +will continue to attempt a reconciliation of the Kustomization with an +exponential backoff, until it succeeds and the Kustomization marked as [ready](#ready-kustomization). + +Note that a Kustomization can be [reconciling](#reconciling-kustomization) +while failing at the same time, for example, due to a newly introduced +configuration issue in the Kustomization spec. When a reconciliation fails, the +`Reconciling` Condition `reason` would be `ProgressingWithRetry`. When the +reconciliation is performed again after the failure, the `reason` is updated to `Progressing`. + +### Inventory + +In order to perform operations such as drift detection, garbage collection, etc. +kustomize-controller needs to keep track of all Kubernetes objects that are +reconciled as part of a Kustomization. To do this, it maintains an inventory +containing the list of Kubernetes resource object references that have been +successfully applied and records it in `.status.inventory`. The inventory +records are in the format `____`. + +```console +Status: + Inventory: + Entries: + Id: default_podinfo__Service + V: v1 + Id: default_podinfo_apps_Deployment + V: v1 + Id: default_podinfo_autoscaling_HorizontalPodAutoscaler + V: v2 +``` + +### Last applied revision + +`.status.lastAppliedRevision` is the last revision of the Artifact from the +referred Source object that was successfully applied to the cluster. + +### Last applied origin revision + +`status.lastAppliedOriginRevision` is the last origin revision of the Artifact +from the referred Source object that was successfully applied to the cluster. + +This field is usually retrieved from the Metadata of the Artifact and depends +on the Source type. For example, for OCI artifacts this is the value associated +with the standard metadata key `org.opencontainers.image.revision`, which is +used to track the revision of the source code that was used to build the OCI +artifact. + +The controller will forward this value when emitting events in the metadata +key `originRevision`. The notification-controller will look for this key in +the event metadata when sending *commit status update* events to Git providers. + +### Last attempted revision + +`.status.lastAttemptedRevision` is the last revision of the Artifact from the +referred Source object that was attempted to be applied to the cluster. + +### Observed Generation + +The kustomize-controller reports an [observed generation][typical-status-properties] +in the Kustomization's `.status.observedGeneration`. The observed generation is +the latest `.metadata.generation` which resulted in either a [ready state](#ready-kustomization), +or stalled due to an error it can not recover from without human +intervention. + +### Last Handled Reconcile At + +The kustomize-controller reports the last `reconcile.fluxcd.io/requestedAt` +annotation value it acted on in the `.status.lastHandledReconcileAt` field. + +For practical information about this field, see [triggering a reconcile](#triggering-a-reconcile). + +[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus diff --git a/docs/spec/v1alpha1/README.md b/docs/spec/v1alpha1/README.md index 43461ff88..e6e4b162d 100644 --- a/docs/spec/v1alpha1/README.md +++ b/docs/spec/v1alpha1/README.md @@ -5,16 +5,16 @@ of Kubernetes objects generated with Kustomize. ## Specification -- [Kustomization CRD](kustomization.md) - + [Source reference](kustomization.md#source-reference) - + [Generate kustomization.yaml](kustomization.md#generate-kustomizationyaml) - + [Reconciliation](kustomization.md#reconciliation) - + [Garbage collection](kustomization.md#garbage-collection) - + [Health assessment](kustomization.md#health-assessment) - + [Kustomization dependencies](kustomization.md#kustomization-dependencies) - + [Role-based access control](kustomization.md#role-based-access-control) - + [Secrets decryption](kustomization.md#secrets-decryption) - + [Status](kustomization.md#status) +- [Kustomization CRD](kustomizations.md) + + [Source reference](kustomizations.md#source-reference) + + [Generate kustomization.yaml](kustomizations.md#generate-kustomizationyaml) + + [Reconciliation](kustomizations.md#reconciliation) + + [Garbage collection](kustomizations.md#garbage-collection) + + [Health assessment](kustomizations.md#health-assessment) + + [Kustomization dependencies](kustomizations.md#kustomization-dependencies) + + [Role-based access control](kustomizations.md#role-based-access-control) + + [Secrets decryption](kustomizations.md#secrets-decryption) + + [Status](kustomizations.md#status) ## Implementation diff --git a/docs/spec/v1alpha1/kustomization.md b/docs/spec/v1alpha1/kustomizations.md similarity index 100% rename from docs/spec/v1alpha1/kustomization.md rename to docs/spec/v1alpha1/kustomizations.md diff --git a/docs/spec/v1beta1/README.md b/docs/spec/v1beta1/README.md index 3d1828421..d58edea3f 100644 --- a/docs/spec/v1beta1/README.md +++ b/docs/spec/v1beta1/README.md @@ -1,23 +1,23 @@ -# kustomize.toolkit.fluxcd.io/v1alpha1 +# kustomize.toolkit.fluxcd.io/v1beta1 This is the v1beta1 API specification for defining continuous delivery pipelines of Kubernetes objects generated with Kustomize. ## Specification -- [Kustomization CRD](kustomization.md) - + [Source reference](kustomization.md#source-reference) - + [Generate kustomization.yaml](kustomization.md#generate-kustomizationyaml) - + [Reconciliation](kustomization.md#reconciliation) - + [Garbage collection](kustomization.md#garbage-collection) - + [Health assessment](kustomization.md#health-assessment) - + [Kustomization dependencies](kustomization.md#kustomization-dependencies) - + [Role-based access control](kustomization.md#role-based-access-control) - + [Override kustomize config](kustomization.md#override-kustomize-config) - + [Variable substitution](kustomization.md#variable-substitution) - + [Targeting remote clusters](kustomization.md#remote-clusters--cluster-api) - + [Secrets decryption](kustomization.md#secrets-decryption) - + [Status](kustomization.md#status) +- [Kustomization CRD](kustomizations.md) + + [Source reference](kustomizations.md#source-reference) + + [Generate kustomization.yaml](kustomizations.md#generate-kustomizationyaml) + + [Reconciliation](kustomizations.md#reconciliation) + + [Garbage collection](kustomizations.md#garbage-collection) + + [Health assessment](kustomizations.md#health-assessment) + + [Kustomization dependencies](kustomizations.md#kustomization-dependencies) + + [Role-based access control](kustomizations.md#role-based-access-control) + + [Override kustomize config](kustomizations.md#override-kustomize-config) + + [Variable substitution](kustomizations.md#variable-substitution) + + [Targeting remote clusters](kustomizations.md#remote-clusters--cluster-api) + + [Secrets decryption](kustomizations.md#secrets-decryption) + + [Status](kustomizations.md#status) ## Implementation diff --git a/docs/spec/v1beta1/kustomization.md b/docs/spec/v1beta1/kustomizations.md similarity index 100% rename from docs/spec/v1beta1/kustomization.md rename to docs/spec/v1beta1/kustomizations.md diff --git a/docs/spec/v1beta2/README.md b/docs/spec/v1beta2/README.md index fa56a3c12..c082b0089 100644 --- a/docs/spec/v1beta2/README.md +++ b/docs/spec/v1beta2/README.md @@ -5,21 +5,21 @@ of Kubernetes objects generated with Kustomize. ## Specification -- [Kustomization CRD](kustomization.md) - + [Example](kustomization.md#example) - + [Recommended settings](kustomization.md#recommended-settings) - + [Source reference](kustomization.md#source-reference) - + [Generate kustomization.yaml](kustomization.md#generate-kustomizationyaml) - + [Reconciliation](kustomization.md#reconciliation) - + [Garbage collection](kustomization.md#garbage-collection) - + [Health assessment](kustomization.md#health-assessment) - + [Kustomization dependencies](kustomization.md#kustomization-dependencies) - + [Role-based access control](kustomization.md#role-based-access-control) - + [Override kustomize config](kustomization.md#override-kustomize-config) - + [Variable substitution](kustomization.md#variable-substitution) - + [Targeting remote clusters](kustomization.md#remote-clusters--cluster-api) - + [Secrets decryption](kustomization.md#secrets-decryption) - + [Status](kustomization.md#status) +- [Kustomization CRD](kustomizations.md) + + [Example](kustomizations.md#example) + + [Recommended settings](kustomizations.md#recommended-settings) + + [Source reference](kustomizations.md#source-reference) + + [Generate kustomization.yaml](kustomizations.md#generate-kustomizationyaml) + + [Reconciliation](kustomizations.md#reconciliation) + + [Garbage collection](kustomizations.md#garbage-collection) + + [Health assessment](kustomizations.md#health-assessment) + + [Kustomization dependencies](kustomizations.md#kustomization-dependencies) + + [Role-based access control](kustomizations.md#role-based-access-control) + + [Override kustomize config](kustomizations.md#override-kustomize-config) + + [Variable substitution](kustomizations.md#variable-substitution) + + [Targeting remote clusters](kustomizations.md#remote-clusters--cluster-api) + + [Secrets decryption](kustomizations.md#secrets-decryption) + + [Status](kustomizations.md#status) ## Implementation diff --git a/docs/spec/v1beta2/kustomization.md b/docs/spec/v1beta2/kustomizations.md similarity index 95% rename from docs/spec/v1beta2/kustomization.md rename to docs/spec/v1beta2/kustomizations.md index 06f145fc7..2a973d174 100644 --- a/docs/spec/v1beta2/kustomization.md +++ b/docs/spec/v1beta2/kustomizations.md @@ -61,14 +61,14 @@ You can run this example by saving the manifest into `podinfo.yaml`. ```console NAME URL READY STATUS - podinfo https://github.com/stefanprodan/podinfo True stored artifact for revision 'master/450796ddb2ab6724ee1cc32a4be56da032d1cca0' + podinfo https://github.com/stefanprodan/podinfo True stored artifact for revision 'master@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0' ``` 3. Run `kubectl get kustomizations` to see the reconciliation status: ```console NAME READY STATUS - podinfo True Applied revision: master/450796ddb2ab6724ee1cc32a4be56da032d1cca0 + podinfo True Applied revision: master@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 ``` 4. Run `kubectl describe kustomization podinfo` to see the reconciliation status conditions and events: @@ -78,7 +78,7 @@ You can run this example by saving the manifest into `podinfo.yaml`. Status: Conditions: Last Transition Time: 2022-06-07T11:14:41Z - Message: Applied revision: master/450796ddb2ab6724ee1cc32a4be56da032d1cca0 + Message: Applied revision: master@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 Reason: ReconciliationSucceeded Status: True Type: Ready @@ -566,6 +566,7 @@ patches and namespace on all the Kubernetes objects reconciled by the resource, offering support for the following Kustomize directives: - [namespace](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/namespace/) +- common [labels](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonlabels/) and [annotations](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonannotations/) - [patches](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patches/) - [images](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/images/) - [components](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/components/) @@ -587,7 +588,36 @@ spec: targetNamespace: test ``` -While the field `targetNamespace` in a Kustomization is optional, if this field is non-empty then the Kubernetes namespace pointed to by `targetNamespace` must exist prior to the Kustomization being applied, kustomize-controller will not create the namespace. +While the field `targetNamespace` in a Kustomization is optional, +if this field is non-empty then the Kubernetes namespace pointed to by `targetNamespace` +must exist prior to the Kustomization being applied, kustomize-controller will not create the namespace. + +### Common Metadata + +With `spec.commonMetadata` you can set common labels and annotations to all resources. + +The main difference to the Kustomize +[commonLabels](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonlabels/) +and [commonAnnotations](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonannotations/), +is that the controller sets the labels and annotations only to the top level `metadata` field, +without patching the Kubernetes Deployment `spec.template` or the Service `spec.selector`. + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: my-app + namespace: apps +spec: + # ...omitted for brevity + commonMetadata: + labels: + app.kubernetes.io/part-of: my-app + annotations: + a8r.io/owner: my-team +``` + +**Note:** Any existing label or annotation will be overridden if its key matches a common one. ### Patches @@ -1398,7 +1428,7 @@ updates its message to report the action performed during a reconciliation run: ```yaml conditions: - lastTransitionTime: "2022-10-17T13:40:21Z" - message: Detecting drift for revision main/a1afe267b54f38b46b487f6e938a6fd508278c07 with a timeout of 50s + message: Detecting drift for revision main@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07 with a timeout of 50s observedGeneration: 2 reason: Progressing status: "True" @@ -1418,12 +1448,12 @@ and the `Ready` condition is set to `True`: status: conditions: - lastTransitionTime: "2022-10-17T13:40:21Z" - message: "Applied revision: main/a1afe267b54f38b46b487f6e938a6fd508278c07" + message: "Applied revision: main@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07" reason: ReconciliationSucceeded status: "True" type: Ready - lastAppliedRevision: main/a1afe267b54f38b46b487f6e938a6fd508278c07 - lastAttemptedRevision: main/a1afe267b54f38b46b487f6e938a6fd508278c07 + lastAppliedRevision: main@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07 + lastAttemptedRevision: main@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07 ``` If `spec.wait` or `spec.healthChecks` is enabled, the health assessment result @@ -1446,7 +1476,7 @@ The controller logs the Kubernetes objects: "msg": "server-side apply completed", "name": "backend", "namespace": "default", - "revision": "main/a1afe267b54f38b46b487f6e938a6fd508278c07", + "revision": "main@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07", "output": { "service/backend": "created", "deployment.apps/backend": "created", @@ -1465,8 +1495,8 @@ status: reason: ValidationFailed status: "False" type: Ready - lastAppliedRevision: master/a1afe267b54f38b46b487f6e938a6fd508278c07 - lastAttemptedRevision: master/7c500d302e38e7e4a3f327343a8a5c21acaaeb87 + lastAppliedRevision: master@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07 + lastAttemptedRevision: master@sha1:7c500d302e38e7e4a3f327343a8a5c21acaaeb87 ``` **Note:** The last applied revision is updated only on a successful reconciliation. @@ -1481,7 +1511,7 @@ When a reconciliation fails, the controller logs the error and issues a Kubernet "msg": "server-side apply completed", "name": "backend", "namespace": "default", - "revision": "main/a1afe267b54f38b46b487f6e938a6fd508278c07", + "revision": "main@sha1:a1afe267b54f38b46b487f6e938a6fd508278c07", "error": "The Service 'backend' is invalid: spec.type: Unsupported value: 'Ingress'" } ``` diff --git a/go.mod b/go.mod index 8df1b6721..c50caa019 100644 --- a/go.mod +++ b/go.mod @@ -1,240 +1,258 @@ module github.com/fluxcd/kustomize-controller -go 1.18 +go 1.24.0 replace github.com/fluxcd/kustomize-controller/api => ./api +// Replace digest lib to master to gather access to BLAKE3. +// xref: https://github.com/opencontainers/go-digest/pull/66 +replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be + require ( - cloud.google.com/go/kms v1.8.0 - filippo.io/age v1.1.1 - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1 - github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 - github.com/aws/aws-sdk-go v1.44.191 - github.com/aws/aws-sdk-go-v2 v1.17.3 - github.com/aws/aws-sdk-go-v2/config v1.18.10 - github.com/aws/aws-sdk-go-v2/credentials v1.13.10 - github.com/aws/aws-sdk-go-v2/service/kms v1.20.1 - github.com/aws/aws-sdk-go-v2/service/sts v1.18.2 - github.com/cyphar/filepath-securejoin v0.2.3 + cloud.google.com/go/kms v1.21.2 + filippo.io/age v1.2.1 + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 + github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 + github.com/cyphar/filepath-securejoin v0.4.1 github.com/dimchansky/utfbom v1.1.1 - github.com/fluxcd/kustomize-controller/api v0.33.0 - github.com/fluxcd/pkg/apis/acl v0.1.0 - github.com/fluxcd/pkg/apis/event v0.3.0 - github.com/fluxcd/pkg/apis/kustomize v0.8.0 - github.com/fluxcd/pkg/apis/meta v0.19.0 - github.com/fluxcd/pkg/http/fetch v0.3.0 - github.com/fluxcd/pkg/kustomize v0.13.0 - github.com/fluxcd/pkg/runtime v0.27.0 - github.com/fluxcd/pkg/ssa v0.23.0 - github.com/fluxcd/pkg/tar v0.2.0 - github.com/fluxcd/pkg/testserver v0.4.0 - github.com/fluxcd/source-controller/api v0.34.0 - github.com/hashicorp/vault/api v1.8.3 - github.com/onsi/gomega v1.26.0 - github.com/ory/dockertest/v3 v3.9.1 - github.com/spf13/pflag v1.0.5 - go.mozilla.org/sops/v3 v3.7.3 - golang.org/x/net v0.5.0 - google.golang.org/api v0.109.0 - google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 - google.golang.org/grpc v1.52.3 - google.golang.org/protobuf v1.28.1 - k8s.io/api v0.26.1 - k8s.io/apiextensions-apiserver v0.26.1 - k8s.io/apimachinery v0.26.1 - k8s.io/client-go v0.26.1 - sigs.k8s.io/cli-utils v0.34.0 - sigs.k8s.io/controller-runtime v0.14.2 - sigs.k8s.io/kustomize/api v0.12.1 - sigs.k8s.io/yaml v1.3.0 + github.com/fluxcd/cli-utils v0.36.0-flux.13 + github.com/fluxcd/kustomize-controller/api v1.6.0 + github.com/fluxcd/pkg/apis/acl v0.7.0 + github.com/fluxcd/pkg/apis/event v0.17.0 + github.com/fluxcd/pkg/apis/kustomize v1.10.0 + github.com/fluxcd/pkg/apis/meta v1.12.0 + github.com/fluxcd/pkg/auth v0.16.0 + github.com/fluxcd/pkg/cache v0.9.0 + github.com/fluxcd/pkg/http/fetch v0.16.0 + github.com/fluxcd/pkg/kustomize v1.18.0 + github.com/fluxcd/pkg/runtime v0.60.0 + github.com/fluxcd/pkg/ssa v0.48.0 + github.com/fluxcd/pkg/tar v0.12.0 + github.com/fluxcd/pkg/testserver v0.11.0 + github.com/fluxcd/source-controller/api v1.6.0 + github.com/getsops/sops/v3 v3.10.2 + github.com/hashicorp/vault/api v1.16.0 + github.com/onsi/gomega v1.37.0 + github.com/opencontainers/go-digest v1.0.0 + github.com/ory/dockertest/v3 v3.12.0 + github.com/spf13/pflag v1.0.6 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.30.0 + k8s.io/api v0.33.0 + k8s.io/apimachinery v0.33.0 + k8s.io/client-go v0.33.0 + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e + sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/kustomize/api v0.19.0 + sigs.k8s.io/yaml v1.4.0 ) -// Pin kustomize to v4.5.7 +// Pin kustomize to v5.6.0 replace ( - sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.12.1 - sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.13.9 + sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.19.0 + sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.19.0 ) -// Fix CVE-2022-32149 -replace golang.org/x/text => golang.org/x/text v0.4.0 - -// Fix CVE-2022-29162 -replace github.com/opencontainers/runc => github.com/opencontainers/runc v1.1.2 - -// Fix CVE-2022-27191 -replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 - -// Fix CVE-2022-1996 (for v2, Go Modules incompatible) -replace github.com/emicklei/go-restful => github.com/emicklei/go-restful v2.16.0+incompatible - // Fix CVE-2022-28948 replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 require ( - cloud.google.com/go/compute v1.14.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.8.0 // indirect - github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.27 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect - github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1 // indirect + cel.dev/expr v0.22.1 // indirect + cloud.google.com/go v0.120.1 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/storage v1.51.0 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 // indirect - github.com/armon/go-metrics v0.3.10 // indirect - github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect - github.com/aws/smithy-go v1.13.5 // indirect + github.com/ProtonMail/go-crypto v1.2.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect + github.com/aws/smithy-go v1.22.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect - github.com/cenkalti/backoff/v3 v3.2.2 // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/continuity v0.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v20.10.14+incompatible // indirect - github.com/docker/docker v20.10.7+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/drone/envsubst v1.0.3 // indirect - github.com/emicklei/go-restful/v3 v3.10.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect + github.com/containerd/continuity v0.4.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v28.1.1+incompatible // indirect + github.com/docker/docker v28.1.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-errors/errors v1.4.2 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-logr/zapr v1.2.3 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/pkg/envsubst v1.4.0 // indirect + github.com/fluxcd/pkg/sourceignore v0.12.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.4.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.23.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.3 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect - github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.5 // indirect - github.com/hashicorp/go-retryablehttp v0.7.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/go-version v1.4.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/vault/sdk v0.7.0 // indirect - github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect - github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef // indirect - github.com/imdario/mergo v0.3.13 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/lib/pq v1.10.5 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/run v1.1.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/opencontainers/runc v1.1.2 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runc v1.2.6 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect - github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/russross/blackfriday v1.6.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect - github.com/spf13/cobra v1.6.1 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/urfave/cli v1.22.16 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xlab/treeprint v1.1.0 // indirect - go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect - go.opencensus.io v0.24.0 // indirect - go.starlark.net v0.0.0-20221028183056-acb66ad56dd2 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.4.0 // indirect - golang.org/x/oauth2 v0.2.0 // indirect - golang.org/x/sys v0.4.0 // indirect - golang.org/x/term v0.4.0 // indirect - golang.org/x/text v0.6.0 // indirect - golang.org/x/time v0.3.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + github.com/zeebo/errs v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/api v0.230.0 // indirect + google.golang.org/genproto v0.0.0-20250425173222-7b384671a197 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect - gopkg.in/urfave/cli.v1 v1.20.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/cli-runtime v0.25.4 // indirect - k8s.io/component-base v0.26.1 // indirect - k8s.io/klog/v2 v2.90.0 // indirect - k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70 // indirect - k8s.io/kubectl v0.25.4 // indirect - k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/cli-runtime v0.33.0 // indirect + k8s.io/component-base v0.33.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/kubectl v0.33.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect ) diff --git a/go.sum b/go.sum index ae76b8a95..0d8947f20 100644 --- a/go.sum +++ b/go.sum @@ -1,1034 +1,608 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/kms v1.8.0 h1:VrJLOsMRzW7IqTTYn+OYupqF3iKSE060Nrn+PECrYjg= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= -filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= -github.com/Azure/azure-sdk-for-go v63.3.0+incompatible h1:INepVujzUrmArRZjDLHbtER+FkvCoEwyRCXGqOlmDII= -github.com/Azure/azure-sdk-for-go v63.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 h1:VuHAcMq8pU1IWNT/m5yRaGqbK0BiQKHT8X4DTp9CHdI= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1 h1:T8quHYlUGyb/oqtSTwqlCr1ilJHrDv+ZtpSfo+hm1BU= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 h1:TOFrNxfjslms5nLLIMjW7N0+zSALX4KiGsptmpb16AA= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0/go.mod h1:EAyXOW1F6BTJPiK2pDvmnvxOHPxoTYWoqBeIlql+QhI= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 h1:Lg6BW0VPmCwcMlvOviL3ruHFO+H9tZNqscK0AeuFjGM= -github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= -github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.20 h1:gJ3E98kMpFB1MFqQCvA1yFab8vthOeD4VlFRQULxahg= -github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1 h1:oPdPEZFSbl7oSPEAIPMPBMUmiL+mqgzBJwM/9qYcwNg= -github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= +c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI= +cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.120.1 h1:Z+5V7yd383+9617XDCyszmK5E4wJRJL+tquMfDj9hLM= +cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0rp8= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE= +cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= +cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= +filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 h1:cSHEbLj0GZeHM1mWG84qEnGFojNEQ83W7cwaPRjcwXU= -github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= -github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= +github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/aws/aws-sdk-go v1.44.191 h1:GnbkalCx/AgobaorDMFCa248acmk+91+aHBQOk7ljzU= -github.com/aws/aws-sdk-go v1.44.191/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY= -github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/config v1.18.10 h1:Znce11DWswdh+5kOsIp+QaNfY9igp1QUN+fZHCKmeCI= -github.com/aws/aws-sdk-go-v2/config v1.18.10/go.mod h1:VATKco+pl+Qe1WW+RzvZTlPPe/09Gg9+vM0ZXsqb16k= -github.com/aws/aws-sdk-go-v2/credentials v1.13.10 h1:T4Y39IhelTLg1f3xiKJssThnFxsndS8B6OnmcXtKK+8= -github.com/aws/aws-sdk-go-v2/credentials v1.13.10/go.mod h1:tqAm4JmQaShel+Qi38hmd1QglSnnxaYt50k/9yGQzzc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k= -github.com/aws/aws-sdk-go-v2/service/kms v1.20.1 h1:3/aZ1EqvVzu8Ska+AmEFvbCjV12GXfVtNqKeluhEYpo= -github.com/aws/aws-sdk-go-v2/service/kms v1.20.1/go.mod h1:13sjgMH7Xu4e46+0BEDhSnNh+cImHSYS5PpBjV3oXcU= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 h1:/2gzjhQowRLarkkBOGPXSRnb8sQ2RVsjdG1C/UliK/c= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.0/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 h1:Jfly6mRxk2ZOSlbCvZfKNS7TukSx1mIzhSsqZ/IGSZI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.2 h1:J/4wIaGInCEYCGhTSruxCxeoA5cy91a+JT7cHFKFSHQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.2/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= -github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72 h1:PcKMOZfp+kNtJTw2HF2op6SjDvwPBYRvz0Y24PQLUR4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72/go.mod h1:vq7/m7dahFXcdzWVOvvjasDI9RcsD3RsTfHmDundJYg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA= +github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= +github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= -github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= -github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= -github.com/docker/cli v20.10.14+incompatible h1:dSBKJOVesDgHo7rbxlYjYsXe7gPzrTT+/cKQgpDAazg= -github.com/docker/cli v20.10.14+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= -github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= -github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= -github.com/emicklei/go-restful/v3 v3.10.0 h1:X4gma4HM7hFm6WMeAsTfqA0GOfdNoCzBIkHGoRLGXuM= -github.com/emicklei/go-restful/v3 v3.10.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= +github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6pH4Q= -github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8= -github.com/fluxcd/pkg/apis/event v0.3.0 h1:B+IXmfSniUGfoczheNAH0YULgS+ejxMl58RyWlvLa1c= -github.com/fluxcd/pkg/apis/event v0.3.0/go.mod h1:xYOOlf+9gCBSYcs93N2XAbJvSVwuVBDBUzqhR+cAo7M= -github.com/fluxcd/pkg/apis/kustomize v0.8.0 h1:A6aLolxPV2Sll44SOHiX96lbXXmRZmS5BoEerkRHrfM= -github.com/fluxcd/pkg/apis/kustomize v0.8.0/go.mod h1:9DPEVSfVIkiC2H3Dk6Ght4YJkswhYIaufXla4tB5Y84= -github.com/fluxcd/pkg/apis/meta v0.19.0 h1:CX75e/eaRWZDTzNdMSWomY1InlssLKcS8GQDSg/aopI= -github.com/fluxcd/pkg/apis/meta v0.19.0/go.mod h1:7b6prDPsViyAzoY7eRfSPS0/MbXpGGsOMvRq2QrTKa4= -github.com/fluxcd/pkg/http/fetch v0.3.0 h1:/mLj0IzTx+GhR09etzMJsBoNQ0qeOx9cSdeUgRB+oqM= -github.com/fluxcd/pkg/http/fetch v0.3.0/go.mod h1:dHTDYIeL0ZAQ9mHM6ZS4VProxho+Atm73MHJ55yj0Sg= -github.com/fluxcd/pkg/kustomize v0.13.0 h1:oC50lpGdz/04aH4dPS/kRBjo+7PUx7BgGsJtSS0CmmE= -github.com/fluxcd/pkg/kustomize v0.13.0/go.mod h1:6vAmxEe/41jBEspGq4OZA/4WlnszPyavm74TGSEVpXg= -github.com/fluxcd/pkg/runtime v0.27.0 h1:zVA95Z0KvNjvZxEZhvIbJyJIwtaiv1aVttHZ4YB/FzY= -github.com/fluxcd/pkg/runtime v0.27.0/go.mod h1:fC1l4Wv1hnsqPKB46eDZBXF8RMZm5FXeU4bnJkwGkqk= -github.com/fluxcd/pkg/ssa v0.23.0 h1:e51n2642tyl8iytYQ68geg8E/6tLJcYqXl83HFrJcr4= -github.com/fluxcd/pkg/ssa v0.23.0/go.mod h1:fbnulY5zeKBC6dXwNIgMc9DfPjEgjfhweL031/9ZFKQ= -github.com/fluxcd/pkg/tar v0.2.0 h1:HEUHgONQYsJGeZZ4x6h5nQU9Aox1I4T3bOp1faWTqf8= -github.com/fluxcd/pkg/tar v0.2.0/go.mod h1:w0/TOC7kwBJhnSJn7TCABkc/I7ib1f2Yz6vOsbLBnhw= -github.com/fluxcd/pkg/testserver v0.4.0 h1:pDZ3gistqYhwlf3sAjn1Q8NzN4Qe6I1BEmHMHi46lMg= -github.com/fluxcd/pkg/testserver v0.4.0/go.mod h1:gjOKX41okmrGYOa4oOF2fiLedDAfPo1XaG/EzrUUGBI= -github.com/fluxcd/source-controller/api v0.34.0 h1:M2kD95IdpmHcDNy78K6T6p7niC38LGwSrKq8XAZEJY0= -github.com/fluxcd/source-controller/api v0.34.0/go.mod h1:w3PDdR+FZyq3zyyUDxz6vY3CKByZfYAjkzJUxuUXCuc= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/cli-utils v0.36.0-flux.13 h1:2X5yjz/rk9mg7+bMFBDZKGKzeZpAmY2s6iwbNZz7OzM= +github.com/fluxcd/cli-utils v0.36.0-flux.13/go.mod h1:b2iSoIeDTtjfCB0IKtGgqlhhvWa1oux3e90CjOf81oA= +github.com/fluxcd/pkg/apis/acl v0.7.0 h1:dMhZJH+g6ZRPjs4zVOAN9vHBd1DcavFgcIFkg5ooOE0= +github.com/fluxcd/pkg/apis/acl v0.7.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ= +github.com/fluxcd/pkg/apis/event v0.17.0 h1:foEINE++pCJlWVhWjYDXfkVmGKu8mQ4BDBlbYi5NU7M= +github.com/fluxcd/pkg/apis/event v0.17.0/go.mod h1:0fLhLFiHlRTDKPDXdRnv+tS7mCMIQ0fJxnEfmvGM/5A= +github.com/fluxcd/pkg/apis/kustomize v1.10.0 h1:47EeSzkQvlQZdH92vHMe2lK2iR8aOSEJq95avw5idts= +github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ= +github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg= +github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI= +github.com/fluxcd/pkg/auth v0.16.0 h1:YEjSaNqlpYoXfoFAGhU/Z8y0322nGsT24W6zCh+sbGw= +github.com/fluxcd/pkg/auth v0.16.0/go.mod h1:+BRnAO61Nr6fACEjJS6eNRdOk1nXhX/FCPylYn1ypNc= +github.com/fluxcd/pkg/cache v0.9.0 h1:EGKfOLMG3fOwWnH/4Axl5xd425mxoQbZzlZoLfd8PDk= +github.com/fluxcd/pkg/cache v0.9.0/go.mod h1:jMwabjWfsC5lW8hE7NM3wtGNwSJ38Javx6EKbEi7INU= +github.com/fluxcd/pkg/envsubst v1.4.0 h1:pYsb6wrmXOSfHXuXQHaaBBMt3LumhgCb8SMdBNAwV/U= +github.com/fluxcd/pkg/envsubst v1.4.0/go.mod h1:zSDFO3Wawi+vI2NPxsMQp+EkIsz/85MNg/s1Wzmqt+s= +github.com/fluxcd/pkg/http/fetch v0.16.0 h1:XzhBTSK5HNdAPEnEGMJHwtoN2LfqQ9QFDsu3DGzl908= +github.com/fluxcd/pkg/http/fetch v0.16.0/go.mod h1:+A+yrOzwA5436ufD8NPeCCQFNzk4metoPUgRVCozvzw= +github.com/fluxcd/pkg/kustomize v1.18.0 h1:wWK+qYwmBmba3N3VAqZ9ijnfVGGaIjcaHWo033URZTw= +github.com/fluxcd/pkg/kustomize v1.18.0/go.mod h1:Ij9722MdWIE6B1EPg2ZJUf6npycgfRfN4Lohi7D/Kic= +github.com/fluxcd/pkg/runtime v0.60.0 h1:d++EkV3FlycB+bzakB5NumwY4J8xts8i7lbvD6jBLeU= +github.com/fluxcd/pkg/runtime v0.60.0/go.mod h1:UeU0/eZLErYC/1bTmgzBfNXhiHy9fuQzjfLK0HxRgxY= +github.com/fluxcd/pkg/sourceignore v0.12.0 h1:jCIe6d50rQ3wdXPF0+PhhqN0XrTRIq3upMomPelI8Mw= +github.com/fluxcd/pkg/sourceignore v0.12.0/go.mod h1:dc0zvkuXM5OgL/b3IkrVuwvPjj1zJn4NBUMH45uJ4Y0= +github.com/fluxcd/pkg/ssa v0.48.0 h1:DW+4DG8L/yZEi30UltOEXPB1d/ZFn4HfVhpJQp5oc2o= +github.com/fluxcd/pkg/ssa v0.48.0/go.mod h1:T50TO0U2obLodZnrFgOrxollfBEy4V673OkM2aTUF1c= +github.com/fluxcd/pkg/tar v0.12.0 h1:og6F+ivnWNRbNJSq0ukCTVs7YrGIlzjxSVZU+E8NprM= +github.com/fluxcd/pkg/tar v0.12.0/go.mod h1:Ra5Cj++MD5iCy7bZGKJJX3GpOeMPv+ZDkPO9bBwpDeU= +github.com/fluxcd/pkg/testserver v0.11.0 h1:a/kxpFqv7XQxZjwVPP3voooRmSd/3ipLVolK0xUIxXQ= +github.com/fluxcd/pkg/testserver v0.11.0/go.mod h1:E8LAH1jW9uClFjTRN27Y/gCCSrzNVx1/w/0NxKuNcas= +github.com/fluxcd/source-controller/api v1.6.0 h1:IxfjUczJ2pzbXIef6iQ0RHEH4AYA9anJfTGK8dzwODM= +github.com/fluxcd/source-controller/api v1.6.0/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws= +github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM= +github.com/getsops/sops/v3 v3.10.2 h1:7t7lBXFcXJPsDMrpYoI36r8xIhjWUmEc8Qdjuwyo+WY= +github.com/getsops/sops/v3 v3.10.2/go.mod h1:Dmtg1qKzFsAl+yqvMgjtnLGTC0l7RnSM6DDtFG7TEsk= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= +github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw= github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= -github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= -github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.8.3 h1:cHQOLcMhBR+aVI0HzhPxO62w2+gJhIrKguQNONPzu6o= -github.com/hashicorp/vault/api v1.8.3/go.mod h1:4g/9lj9lmuJQMtT6CmVMHC5FW1yENaVv+Nv4ZfG8fAg= -github.com/hashicorp/vault/sdk v0.7.0 h1:2pQRO40R1etpKkia5fb4kjrdYMx3BHklPxl1pxpxDHg= -github.com/hashicorp/vault/sdk v0.7.0/go.mod h1:KyfArJkhooyba7gYCKSq8v66QdqJmnbAxtV/OX1+JTs= -github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I= -github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= -github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= -github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= -github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c h1:RC8WMpjonrBfyAh6VN/POIPtYD5tRAq0qMqCRjQNK+g= -github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c/go.mod h1:9OcmHNQQUTbk4XCffrLgN1NEKc2mh5u++biHVrvHsSU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= -github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v1.1.2 h1:2VSZwLx5k/BfsBxMMipG/LYUnmqOD/BPkIVgQUcTlLw= -github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= -github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= -github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= -github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= -github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be h1:f2PlhC9pm5sqpBZFvnAoKj+KzXRzbjFMA+TqXfJdgho= +github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b h1:nAiL9bmUK4IzFrKoVMRykv0iYGdoit5vpbPaVCZ+fI4= +github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.2.6 h1:P7Hqg40bsMvQGCS4S7DJYhUZOISMLJOB2iGX5COWiPk= +github.com/opencontainers/runc v1.2.6/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= +github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= -github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a h1:N7VD+PwpJME2ZfQT8+ejxwA4Ow10IkGbU0MGf94ll8k= -go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a/go.mod h1:YDKUvO0b//78PaaEro6CAPH6NqohCmL2Cwju5XI2HoE= -go.mozilla.org/sops/v3 v3.7.3 h1:CYx02LnWTATWv6NqWJIt4JCKVKSnGV+MsRiDpvwWQhg= -go.mozilla.org/sops/v3 v3.7.3/go.mod h1:AutdccISG5Nt/faUigaKPU9aGmhyZuCyUiSx5YCa1O8= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.starlark.net v0.0.0-20221028183056-acb66ad56dd2 h1:5/KzhcSqd4UgY51l17r7C5g/JiE6DRw1Vq7VJfQHuMc= -go.starlark.net v0.0.0-20221028183056-acb66ad56dd2/go.mod h1:kIVgS18CjmEC3PqMd5kaJSGEifyV/CeB9x506ZJ1Vbk= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= -golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= -golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.109.0 h1:sW9hgHyX497PP5//NUM7nqfV8D0iDfBApqq7sOh1XR8= -google.golang.org/api v0.109.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 h1:p0kMzw6AG0JEzd7Z+kXqOiLhC6gjUQTbtS2zR0Q3DbI= -google.golang.org/genproto v0.0.0-20230131230820-1c016267d619/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= -google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM= +google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ= +google.golang.org/genproto v0.0.0-20250425173222-7b384671a197 h1:qWb9n6MA4nHA/g2varvEG/jTCs8zUuSa+5VqFgX2K+0= +google.golang.org/genproto v0.0.0-20250425173222-7b384671a197/go.mod h1:Cej/8iHf9mPl71o/a+R1rrvSFrAAVCUFX9s/sbNttBc= +google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197 h1:9DuBh3k1jUho2DHdxH+kbJwthIAq02vGvZNrD2ggF+Y= +google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 h1:29cjnHVylHwTzH66WfFZqgSQgnxzvWE+jvBwpZCLRxY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= -gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ= -k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg= -k8s.io/apiextensions-apiserver v0.26.1 h1:cB8h1SRk6e/+i3NOrQgSFij1B2S0Y0wDoNl66bn8RMI= -k8s.io/apiextensions-apiserver v0.26.1/go.mod h1:AptjOSXDGuE0JICx/Em15PaoO7buLwTs0dGleIHixSM= -k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ= -k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/cli-runtime v0.25.4 h1:GTSBN7aKBrc2LqpdO30CmHQqJtRmotxV7XsMSP+QZIk= -k8s.io/cli-runtime v0.25.4/go.mod h1:JGOw1CR8v4Mcz6cEKA7bFQe0bPrNn1l5sGAX1/Ke4Eg= -k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU= -k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE= -k8s.io/component-base v0.26.1 h1:4ahudpeQXHZL5kko+iDHqLj/FSGAEUnSVO0EBbgDd+4= -k8s.io/component-base v0.26.1/go.mod h1:VHrLR0b58oC035w6YQiBSbtsf0ThuSwXP+p5dD/kAWU= -k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= -k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70 h1:zfqQc1V6/ZgGpvrOVvr62OjiqQX4lZjfznK34NQwkqw= -k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= -k8s.io/kubectl v0.25.4 h1:O3OA1z4V1ZyvxCvScjq0pxAP7ABgznr8UvnVObgI6Dc= -k8s.io/kubectl v0.25.4/go.mod h1:CKMrQ67Bn2YCP26tZStPQGq62zr9pvzEf65A0navm8k= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/cli-utils v0.34.0 h1:zCUitt54f0/MYj/ajVFnG6XSXMhpZ72O/3RewIchW8w= -sigs.k8s.io/cli-utils v0.34.0/go.mod h1:EXyMwPMu9OL+LRnj0JEMsGG/fRvbgFadcVlSnE8RhFs= -sigs.k8s.io/controller-runtime v0.14.2 h1:P6IwDhbsRWsBClt/8/h8Zy36bCuGuW5Op7MHpFrN/60= -sigs.k8s.io/controller-runtime v0.14.2/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= -sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= -sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= -sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/cli-runtime v0.33.0 h1:Lbl/pq/1o8BaIuyn+aVLdEPHVN665tBAXUePs8wjX7c= +k8s.io/cli-runtime v0.33.0/go.mod h1:QcA+r43HeUM9jXFJx7A+yiTPfCooau/iCcP1wQh4NFw= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= +k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g= +k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/api-docs/template/pkg.tpl b/hack/api-docs/template/pkg.tpl index 61d05f776..d3c3d0731 100644 --- a/hack/api-docs/template/pkg.tpl +++ b/hack/api-docs/template/pkg.tpl @@ -1,5 +1,10 @@ {{ define "packages" }} -

Kustomize API reference

+

Kustomize API reference + {{- with (index .packages 0) -}} + {{ with (index .GoPackages 0 ) -}} + {{ printf " %s" .Name -}} + {{ end -}} + {{ end }}

{{ with .packages}}

Packages:

diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 923f609ed..e4b53a5f0 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2021 The Flux authors +Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/cache/operations.go b/internal/cache/operations.go new file mode 100644 index 000000000..cabfbf8ef --- /dev/null +++ b/internal/cache/operations.go @@ -0,0 +1,29 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package intcache + +const ( + OperationDecryptWithAWS = "decrypt_with_aws" + OperationDecryptWithAzure = "decrypt_with_azure" + OperationDecryptWithGCP = "decrypt_with_gcp" +) + +var AllOperations = []string{ + OperationDecryptWithAWS, + OperationDecryptWithAzure, + OperationDecryptWithGCP, +} diff --git a/internal/controller/constants.go b/internal/controller/constants.go new file mode 100644 index 000000000..ed9a9866f --- /dev/null +++ b/internal/controller/constants.go @@ -0,0 +1,19 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +const OCIArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision" diff --git a/controllers/kustomization_acl_test.go b/internal/controller/kustomization_acl_test.go similarity index 94% rename from controllers/kustomization_acl_test.go rename to internal/controller/kustomization_acl_test.go index dd587f5a0..f99b1c762 100644 --- a/controllers/kustomization_acl_test.go +++ b/internal/controller/kustomization_acl_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -22,16 +22,17 @@ import ( "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" apiacl "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_NoCrossNamespaceRefs(t *testing.T) { @@ -114,7 +115,7 @@ stringData: return resultK.Status.LastAppliedRevision == revision }, timeout, time.Second).Should(BeTrue()) - g.Expect(readyCondition.Reason).To(Equal(kustomizev1.ReconciliationSucceededReason)) + g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationSucceededReason)) }) t.Run("fails to reconcile from cross-namespace source", func(t *testing.T) { diff --git a/internal/controller/kustomization_configuration_error_test.go b/internal/controller/kustomization_configuration_error_test.go new file mode 100644 index 000000000..83ceffd4d --- /dev/null +++ b/internal/controller/kustomization_configuration_error_test.go @@ -0,0 +1,216 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "testing" + "time" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/pkg/apis/kustomize" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/auth" + "github.com/fluxcd/pkg/runtime/conditions" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/kustomize-controller/internal/decryptor" +) + +func TestKustomizationReconciler_ConfigurationError(t *testing.T) { + g := NewWithT(t) + id := "invalid-config-" + randStringRunes(5) + revision := "v1.0.0" + resultK := &kustomizev1.Kustomization{} + timeout := 60 * time.Second + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + manifests := func(name string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: {} +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + t.Run("invalid cel expression", func(t *testing.T) { + g := NewWithT(t) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + TargetNamespace: id, + Interval: metav1.Duration{Duration: 2 * time.Minute}, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + Prune: true, + Timeout: &metav1.Duration{Duration: time.Second}, + Wait: true, + HealthCheckExprs: []kustomize.CustomHealthCheck{{ + APIVersion: "v1", + Kind: "ConfigMap", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + InProgress: "foo.", + Current: "true", + }, + }}, + }, + } + + err = k8sClient.Create(context.Background(), kustomization) + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.IsFalse(resultK, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(resultK.Status.ObservedGeneration).To(Equal(resultK.GetGeneration())) + + g.Expect(conditions.IsTrue(resultK, meta.StalledCondition)).To(BeTrue()) + for _, cond := range []string{meta.ReadyCondition, meta.StalledCondition} { + g.Expect(conditions.GetReason(resultK, cond)).To(Equal(meta.InvalidCELExpressionReason)) + g.Expect(conditions.GetMessage(resultK, cond)).To(ContainSubstring( + "failed to create custom status evaluator for healthchecks[0]: failed to parse the expression InProgress: failed to parse the CEL expression 'foo.': ERROR: :1:5: Syntax error: no viable alternative at input '.'")) + } + }) + + t.Run("object level workload identity feature gate disabled", func(t *testing.T) { + g := NewWithT(t) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + TargetNamespace: id, + Interval: metav1.Duration{Duration: 2 * time.Minute}, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + Prune: true, + Decryption: &kustomizev1.Decryption{ + Provider: decryptor.DecryptionProviderSOPS, + ServiceAccountName: "foo", + }, + }, + } + + err = k8sClient.Create(context.Background(), kustomization) + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.IsFalse(resultK, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + + // In this case the controller does not update the observed generation + // because if the feature gate is enabled then the generation of the + // object can be properly observed. + g.Expect(resultK.Status.ObservedGeneration).To(Equal(int64(-1))) + + g.Expect(conditions.IsTrue(resultK, meta.StalledCondition)).To(BeTrue()) + for _, cond := range []string{meta.ReadyCondition, meta.StalledCondition} { + g.Expect(conditions.GetReason(resultK, cond)).To(Equal(meta.FeatureGateDisabledReason)) + g.Expect(conditions.GetMessage(resultK, cond)).To(ContainSubstring( + "to use spec.decryption.serviceAccountName for decryption authentication please enable the ObjectLevelWorkloadIdentity feature gate in the controller")) + } + }) + + t.Run("object level workload identity feature gate enabled", func(t *testing.T) { + g := NewWithT(t) + + t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true") + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + TargetNamespace: id, + Interval: metav1.Duration{Duration: 2 * time.Minute}, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + Prune: true, + Decryption: &kustomizev1.Decryption{ + Provider: decryptor.DecryptionProviderSOPS, + ServiceAccountName: "foo", + }, + }, + } + + err = k8sClient.Create(context.Background(), kustomization) + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.IsTrue(resultK, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + }) +} diff --git a/controllers/kustomization_controller.go b/internal/controller/kustomization_controller.go similarity index 60% rename from controllers/kustomization_controller.go rename to internal/controller/kustomization_controller.go index 60355927f..b64c04b95 100644 --- a/controllers/kustomization_controller.go +++ b/internal/controller/kustomization_controller.go @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "bytes" "context" + "errors" "fmt" "os" "sort" @@ -35,8 +36,7 @@ import ( "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" kuberecorder "k8s.io/client-go/tools/record" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" - "sigs.k8s.io/cli-utils/pkg/object" + "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,27 +44,37 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/ratelimiter" - "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/cli-utils/pkg/object" apiacl "github.com/fluxcd/pkg/apis/acl" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/auth" + "github.com/fluxcd/pkg/cache" "github.com/fluxcd/pkg/http/fetch" + generator "github.com/fluxcd/pkg/kustomize" "github.com/fluxcd/pkg/runtime/acl" + "github.com/fluxcd/pkg/runtime/cel" runtimeClient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/pkg/runtime/conditions" runtimeCtrl "github.com/fluxcd/pkg/runtime/controller" + "github.com/fluxcd/pkg/runtime/jitter" "github.com/fluxcd/pkg/runtime/patch" "github.com/fluxcd/pkg/runtime/predicates" + "github.com/fluxcd/pkg/runtime/statusreaders" "github.com/fluxcd/pkg/ssa" + "github.com/fluxcd/pkg/ssa/normalize" + ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/pkg/tar" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + intcache "github.com/fluxcd/kustomize-controller/internal/cache" "github.com/fluxcd/kustomize-controller/internal/decryptor" "github.com/fluxcd/kustomize-controller/internal/inventory" - generator "github.com/fluxcd/pkg/kustomize" ) // +kubebuilder:rbac:groups=kustomize.toolkit.fluxcd.io,resources=kustomizations,verbs=get;list;watch;create;update;patch;delete @@ -73,6 +83,7 @@ import ( // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets;ocirepositories;gitrepositories,verbs=get;list;watch // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status;ocirepositories/status;gitrepositories/status,verbs=get // +kubebuilder:rbac:groups="",resources=configmaps;secrets;serviceaccounts,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // KustomizationReconciler reconciles a Kustomization object @@ -81,27 +92,34 @@ type KustomizationReconciler struct { kuberecorder.EventRecorder runtimeCtrl.Metrics - artifactFetcher *fetch.ArchiveFetcher - requeueDependency time.Duration - StatusPoller *polling.StatusPoller - PollingOpts polling.Options - ControllerName string - statusManager string - NoCrossNamespaceRefs bool - NoRemoteBases bool - DefaultServiceAccount string - KubeConfigOpts runtimeClient.KubeConfigOptions + artifactFetchRetries int + requeueDependency time.Duration + + Mapper apimeta.RESTMapper + APIReader client.Reader + ClusterReader engine.ClusterReaderFactory + ControllerName string + statusManager string + NoCrossNamespaceRefs bool + NoRemoteBases bool + FailFast bool + DefaultServiceAccount string + KubeConfigOpts runtimeClient.KubeConfigOptions + ConcurrentSSA int + DisallowedFieldManagers []string + StrictSubstitutions bool + GroupChangeLog bool + TokenCache *cache.TokenCache } // KustomizationReconcilerOptions contains options for the KustomizationReconciler. type KustomizationReconcilerOptions struct { - MaxConcurrentReconciles int HTTPRetry int DependencyRequeueInterval time.Duration - RateLimiter ratelimiter.RateLimiter + RateLimiter workqueue.TypedRateLimiter[reconcile.Request] } -func (r *KustomizationReconciler) SetupWithManager(mgr ctrl.Manager, opts KustomizationReconcilerOptions) error { +func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts KustomizationReconcilerOptions) error { const ( ociRepositoryIndexKey string = ".metadata.ociRepository" gitRepositoryIndexKey string = ".metadata.gitRepository" @@ -109,56 +127,48 @@ func (r *KustomizationReconciler) SetupWithManager(mgr ctrl.Manager, opts Kustom ) // Index the Kustomizations by the OCIRepository references they (may) point at. - if err := mgr.GetCache().IndexField(context.TODO(), &kustomizev1.Kustomization{}, ociRepositoryIndexKey, + if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, ociRepositoryIndexKey, r.indexBy(sourcev1.OCIRepositoryKind)); err != nil { return fmt.Errorf("failed setting index fields: %w", err) } // Index the Kustomizations by the GitRepository references they (may) point at. - if err := mgr.GetCache().IndexField(context.TODO(), &kustomizev1.Kustomization{}, gitRepositoryIndexKey, + if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, gitRepositoryIndexKey, r.indexBy(sourcev1.GitRepositoryKind)); err != nil { return fmt.Errorf("failed setting index fields: %w", err) } // Index the Kustomizations by the Bucket references they (may) point at. - if err := mgr.GetCache().IndexField(context.TODO(), &kustomizev1.Kustomization{}, bucketIndexKey, + if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, bucketIndexKey, r.indexBy(sourcev1.BucketKind)); err != nil { return fmt.Errorf("failed setting index fields: %w", err) } r.requeueDependency = opts.DependencyRequeueInterval r.statusManager = fmt.Sprintf("gotk-%s", r.ControllerName) - r.artifactFetcher = fetch.NewArchiveFetcher( - opts.HTTPRetry, - tar.UnlimitedUntarSize, - tar.UnlimitedUntarSize, - os.Getenv("SOURCE_CONTROLLER_LOCALHOST"), - ) + r.artifactFetchRetries = opts.HTTPRetry - recoverPanic := true return ctrl.NewControllerManagedBy(mgr). For(&kustomizev1.Kustomization{}, builder.WithPredicates( predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}), )). Watches( - &source.Kind{Type: &sourcev1.OCIRepository{}}, + &sourcev1.OCIRepository{}, handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(ociRepositoryIndexKey)), builder.WithPredicates(SourceRevisionChangePredicate{}), ). Watches( - &source.Kind{Type: &sourcev1.GitRepository{}}, + &sourcev1.GitRepository{}, handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(gitRepositoryIndexKey)), builder.WithPredicates(SourceRevisionChangePredicate{}), ). Watches( - &source.Kind{Type: &sourcev1.Bucket{}}, + &sourcev1.Bucket{}, handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(bucketIndexKey)), builder.WithPredicates(SourceRevisionChangePredicate{}), ). WithOptions(controller.Options{ - MaxConcurrentReconciles: opts.MaxConcurrentReconciles, - RateLimiter: opts.RateLimiter, - RecoverPanic: &recoverPanic, + RateLimiter: opts.RateLimiter, }). Complete(r) } @@ -183,9 +193,12 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // Record Prometheus metrics. - r.Metrics.RecordReadiness(ctx, obj) r.Metrics.RecordDuration(ctx, obj, reconcileStart) - r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend) + + // Do not proceed if the Kustomization is suspended + if obj.Spec.Suspend { + return + } // Log and emit success event. if conditions.IsReady(obj) { @@ -193,35 +206,62 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques time.Since(reconcileStart).String(), obj.Spec.Interval.Duration.String()) log.Info(msg, "revision", obj.Status.LastAttemptedRevision) - r.event(obj, obj.Status.LastAppliedRevision, eventv1.EventSeverityInfo, msg, + r.event(obj, obj.Status.LastAppliedRevision, obj.Status.LastAppliedOriginRevision, eventv1.EventSeverityInfo, msg, map[string]string{ kustomizev1.GroupVersion.Group + "/" + eventv1.MetaCommitStatusKey: eventv1.MetaCommitStatusUpdateValue, }) } }() + // Prune managed resources if the object is under deletion. + if !obj.ObjectMeta.DeletionTimestamp.IsZero() { + return r.finalize(ctx, obj) + } + // Add finalizer first if it doesn't exist to avoid the race condition // between init and delete. + // Note: Finalizers in general can only be added when the deletionTimestamp + // is not set. if !controllerutil.ContainsFinalizer(obj, kustomizev1.KustomizationFinalizer) { controllerutil.AddFinalizer(obj, kustomizev1.KustomizationFinalizer) return ctrl.Result{Requeue: true}, nil } - // Prune managed resources if the object is under deletion. - if !obj.ObjectMeta.DeletionTimestamp.IsZero() { - return r.finalize(ctx, obj) - } - // Skip reconciliation if the object is suspended. if obj.Spec.Suspend { log.Info("Reconciliation is suspended for this object") return ctrl.Result{}, nil } + // Configure custom health checks. + statusReaders, err := cel.PollerWithCustomHealthChecks(ctx, obj.Spec.HealthCheckExprs) + if err != nil { + const msg = "Reconciliation failed terminally due to configuration error" + errMsg := fmt.Sprintf("%s: %v", msg, err) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidCELExpressionReason, "%s", errMsg) + conditions.MarkStalled(obj, meta.InvalidCELExpressionReason, "%s", errMsg) + obj.Status.ObservedGeneration = obj.Generation + log.Error(err, msg) + r.event(obj, "", "", eventv1.EventSeverityError, errMsg, nil) + return ctrl.Result{}, nil + } + + // Check object-level workload identity feature gate. + if d := obj.Spec.Decryption; d != nil && d.ServiceAccountName != "" && !auth.IsObjectLevelWorkloadIdentityEnabled() { + const gate = auth.FeatureGateObjectLevelWorkloadIdentity + const msgFmt = "to use spec.decryption.serviceAccountName for decryption authentication please enable the %s feature gate in the controller" + msg := fmt.Sprintf(msgFmt, gate) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.FeatureGateDisabledReason, msgFmt, gate) + conditions.MarkStalled(obj, meta.FeatureGateDisabledReason, msgFmt, gate) + log.Error(auth.ErrObjectLevelWorkloadIdentityNotEnabled, msg) + r.event(obj, "", "", eventv1.EventSeverityError, msg, nil) + return ctrl.Result{}, nil + } + // Resolve the source reference and requeue the reconciliation if the source is not found. artifactSource, err := r.getSource(ctx, obj) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ArtifactFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", err) if apierrors.IsNotFound(err) { msg := fmt.Sprintf("Source '%s' not found", obj.Spec.SourceRef.String()) @@ -230,43 +270,45 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques } if acl.IsAccessDenied(err) { - conditions.MarkFalse(obj, meta.ReadyCondition, apiacl.AccessDeniedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, apiacl.AccessDeniedReason, "%s", err) log.Error(err, "Access denied to cross-namespace source") - r.event(obj, "unknown", eventv1.EventSeverityError, err.Error(), nil) + r.event(obj, "", "", eventv1.EventSeverityError, err.Error(), nil) return ctrl.Result{RequeueAfter: obj.GetRetryInterval()}, nil } // Retry with backoff on transient errors. - return ctrl.Result{Requeue: true}, err + return ctrl.Result{}, err } // Requeue the reconciliation if the source artifact is not found. if artifactSource.GetArtifact() == nil { - msg := "Source is not ready, artifact not found" - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ArtifactFailedReason, msg) + msg := fmt.Sprintf("Source artifact not found, retrying in %s", r.requeueDependency.String()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", msg) log.Info(msg) - return ctrl.Result{RequeueAfter: obj.GetRetryInterval()}, nil + return ctrl.Result{RequeueAfter: r.requeueDependency}, nil } + revision := artifactSource.GetArtifact().Revision + originRevision := getOriginRevision(artifactSource) // Check dependencies and requeue the reconciliation if the check fails. if len(obj.Spec.DependsOn) > 0 { if err := r.checkDependencies(ctx, obj, artifactSource); err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.DependencyNotReadyReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.DependencyNotReadyReason, "%s", err) msg := fmt.Sprintf("Dependencies do not meet ready condition, retrying in %s", r.requeueDependency.String()) log.Info(msg) - r.event(obj, artifactSource.GetArtifact().Revision, eventv1.EventSeverityInfo, msg, nil) + r.event(obj, revision, originRevision, eventv1.EventSeverityInfo, msg, nil) return ctrl.Result{RequeueAfter: r.requeueDependency}, nil } log.Info("All dependencies are ready, proceeding with reconciliation") } // Reconcile the latest revision. - reconcileErr := r.reconcile(ctx, obj, artifactSource, patcher) + reconcileErr := r.reconcile(ctx, obj, artifactSource, patcher, statusReaders) // Requeue at the specified retry interval if the artifact tarball is not found. - if reconcileErr == fetch.FileNotFoundError { + if errors.Is(reconcileErr, fetch.ErrFileNotFound) { msg := fmt.Sprintf("Source is not ready, artifact not found, retrying in %s", r.requeueDependency.String()) - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ArtifactFailedReason, msg) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", msg) log.Info(msg) return ctrl.Result{RequeueAfter: r.requeueDependency}, nil } @@ -277,31 +319,32 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques time.Since(reconcileStart).String(), obj.GetRetryInterval().String()), "revision", - artifactSource.GetArtifact().Revision) - r.event(obj, artifactSource.GetArtifact().Revision, eventv1.EventSeverityError, + revision) + r.event(obj, revision, originRevision, eventv1.EventSeverityError, reconcileErr.Error(), nil) return ctrl.Result{RequeueAfter: obj.GetRetryInterval()}, nil } // Requeue the reconciliation at the specified interval. - return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil + return ctrl.Result{RequeueAfter: jitter.JitteredIntervalDuration(obj.GetRequeueAfter())}, nil } func (r *KustomizationReconciler) reconcile( ctx context.Context, obj *kustomizev1.Kustomization, src sourcev1.Source, - patcher *patch.SerialPatcher) error { - - revision := src.GetArtifact().Revision - isNewRevision := obj.Status.LastAppliedRevision != revision + patcher *patch.SerialPatcher, + statusReaders []func(apimeta.RESTMapper) engine.StatusReader) error { + log := ctrl.LoggerFrom(ctx) // Update status with the reconciliation progress. + revision := src.GetArtifact().Revision + originRevision := getOriginRevision(src) progressingMsg := fmt.Sprintf("Fetching manifests for revision %s with a timeout of %s", revision, obj.GetTimeout().String()) - conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "Reconciliation in progress") - conditions.MarkReconciling(obj, meta.ProgressingReason, progressingMsg) + conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "%s", "Reconciliation in progress") + conditions.MarkReconciling(obj, meta.ProgressingReason, "%s", progressingMsg) if err := r.patch(ctx, obj, patcher); err != nil { - return fmt.Errorf("failed to update status, error: %w", err) + return fmt.Errorf("failed to update status: %w", err) } // Create a snapshot of the current inventory. @@ -314,82 +357,104 @@ func (r *KustomizationReconciler) reconcile( tmpDir, err := MkdirTempAbs("", "kustomization-") if err != nil { err = fmt.Errorf("tmp dir error: %w", err) - conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.DirCreationFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.DirCreationFailedReason, "%s", err) return err } - defer os.RemoveAll(tmpDir) + defer func(path string) { + if err := os.RemoveAll(path); err != nil { + log.Error(err, "failed to remove tmp dir", "path", path) + } + }(tmpDir) // Download artifact and extract files to the tmp dir. - err = r.artifactFetcher.Fetch(src.GetArtifact().URL, src.GetArtifact().Checksum, tmpDir) - if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ArtifactFailedReason, err.Error()) + if err = fetch.NewArchiveFetcherWithLogger( + r.artifactFetchRetries, + tar.UnlimitedUntarSize, + tar.UnlimitedUntarSize, + os.Getenv("SOURCE_CONTROLLER_LOCALHOST"), + ctrl.LoggerFrom(ctx), + ).Fetch(src.GetArtifact().URL, src.GetArtifact().Digest, tmpDir); err != nil { + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", err) return err } // check build path exists dirPath, err := securejoin.SecureJoin(tmpDir, obj.Spec.Path) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ArtifactFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", err) return err } if _, err := os.Stat(dirPath); err != nil { err = fmt.Errorf("kustomization path not found: %w", err) - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ArtifactFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", err) return err } // Report progress and set last attempted revision in status. obj.Status.LastAttemptedRevision = revision progressingMsg = fmt.Sprintf("Building manifests for revision %s with a timeout of %s", revision, obj.GetTimeout().String()) - conditions.MarkReconciling(obj, meta.ProgressingReason, progressingMsg) + conditions.MarkReconciling(obj, meta.ProgressingReason, "%s", progressingMsg) if err := r.patch(ctx, obj, patcher); err != nil { - return fmt.Errorf("failed to update status, error: %w", err) + return fmt.Errorf("failed to update status: %w", err) } // Configure the Kubernetes client for impersonation. - impersonation := runtimeClient.NewImpersonator( - r.Client, - r.StatusPoller, - r.PollingOpts, - obj.Spec.KubeConfig, - r.KubeConfigOpts, - r.DefaultServiceAccount, - obj.Spec.ServiceAccountName, - obj.GetNamespace(), - ) + var impersonatorOpts []runtimeClient.ImpersonatorOption + var mustImpersonate bool + if r.DefaultServiceAccount != "" || obj.Spec.ServiceAccountName != "" { + mustImpersonate = true + impersonatorOpts = append(impersonatorOpts, + runtimeClient.WithServiceAccount(r.DefaultServiceAccount, obj.Spec.ServiceAccountName, obj.GetNamespace())) + } + if obj.Spec.KubeConfig != nil { + mustImpersonate = true + impersonatorOpts = append(impersonatorOpts, + runtimeClient.WithKubeConfig(obj.Spec.KubeConfig, r.KubeConfigOpts, obj.GetNamespace())) + } + if r.ClusterReader != nil || len(statusReaders) > 0 { + impersonatorOpts = append(impersonatorOpts, + runtimeClient.WithPolling(r.ClusterReader, statusReaders...)) + } + impersonation := runtimeClient.NewImpersonator(r.Client, impersonatorOpts...) // Create the Kubernetes client that runs under impersonation. - kubeClient, statusPoller, err := impersonation.GetClient(ctx) + var kubeClient client.Client + var statusPoller *polling.StatusPoller + if mustImpersonate { + kubeClient, statusPoller, err = impersonation.GetClient(ctx) + } else { + kubeClient, statusPoller = r.getClientAndPoller(statusReaders) + } if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ReconciliationFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ReconciliationFailedReason, "%s", err) return fmt.Errorf("failed to build kube client: %w", err) } // Generate kustomization.yaml if needed. k, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.BuildFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.BuildFailedReason, "%s", err) return err } err = r.generate(unstructured.Unstructured{Object: k}, tmpDir, dirPath) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.BuildFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.BuildFailedReason, "%s", err) return err } // Build the Kustomize overlay and decrypt secrets if needed. resources, err := r.build(ctx, obj, unstructured.Unstructured{Object: k}, tmpDir, dirPath) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.BuildFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.BuildFailedReason, "%s", err) return err } // Convert the build result into Kubernetes unstructured objects. - objects, err := ssa.ReadObjects(bytes.NewReader(resources)) + objects, err := ssautil.ReadObjects(bytes.NewReader(resources)) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.BuildFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.BuildFailedReason, "%s", err) return err } @@ -399,18 +464,19 @@ func (r *KustomizationReconciler) reconcile( Group: kustomizev1.GroupVersion.Group, }) resourceManager.SetOwnerLabels(objects, obj.GetName(), obj.GetNamespace()) + resourceManager.SetConcurrency(r.ConcurrentSSA) // Update status with the reconciliation progress. progressingMsg = fmt.Sprintf("Detecting drift for revision %s with a timeout of %s", revision, obj.GetTimeout().String()) - conditions.MarkReconciling(obj, meta.ProgressingReason, progressingMsg) + conditions.MarkReconciling(obj, meta.ProgressingReason, "%s", progressingMsg) if err := r.patch(ctx, obj, patcher); err != nil { - return fmt.Errorf("failed to update status, error: %w", err) + return fmt.Errorf("failed to update status: %w", err) } // Validate and apply resources in stages. - drifted, changeSet, err := r.apply(ctx, resourceManager, obj, revision, objects) + drifted, changeSet, err := r.apply(ctx, resourceManager, obj, revision, originRevision, objects) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ReconciliationFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ReconciliationFailedReason, "%s", err) return err } @@ -418,7 +484,7 @@ func (r *KustomizationReconciler) reconcile( newInventory := inventory.New() err = inventory.AddChangeSet(newInventory, changeSet) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ReconciliationFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ReconciliationFailedReason, "%s", err) return err } @@ -428,37 +494,40 @@ func (r *KustomizationReconciler) reconcile( // Detect stale resources which are subject to garbage collection. staleObjects, err := inventory.Diff(oldInventory, newInventory) if err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.ReconciliationFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.ReconciliationFailedReason, "%s", err) return err } // Run garbage collection for stale resources that do not have pruning disabled. - if _, err := r.prune(ctx, resourceManager, obj, revision, staleObjects); err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.PruneFailedReason, err.Error()) + if _, err := r.prune(ctx, resourceManager, obj, revision, originRevision, staleObjects); err != nil { + conditions.MarkFalse(obj, meta.ReadyCondition, meta.PruneFailedReason, "%s", err) return err } // Run the health checks for the last applied resources. + isNewRevision := !src.GetArtifact().HasRevision(obj.Status.LastAppliedRevision) if err := r.checkHealth(ctx, resourceManager, patcher, obj, revision, + originRevision, isNewRevision, drifted, changeSet.ToObjMetadataSet()); err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.HealthCheckFailedReason, err.Error()) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.HealthCheckFailedReason, "%s", err) return err } - // Set last applied revision. + // Set last applied revisions. obj.Status.LastAppliedRevision = revision + obj.Status.LastAppliedOriginRevision = originRevision // Mark the object as ready. conditions.MarkTrue(obj, meta.ReadyCondition, - kustomizev1.ReconciliationSucceededReason, - fmt.Sprintf("Applied revision: %s", revision)) + meta.ReconciliationSucceededReason, + "Applied revision: %s", revision) return nil } @@ -475,7 +544,7 @@ func (r *KustomizationReconciler) checkDependencies(ctx context.Context, Name: d.Name, } var k kustomizev1.Kustomization - err := r.Get(ctx, dName, &k) + err := r.APIReader.Get(ctx, dName, &k) if err != nil { return fmt.Errorf("dependency '%s' not found: %w", dName, err) } @@ -488,10 +557,19 @@ func (r *KustomizationReconciler) checkDependencies(ctx context.Context, return fmt.Errorf("dependency '%s' is not ready", dName) } + srcNamespace := k.Spec.SourceRef.Namespace + if srcNamespace == "" { + srcNamespace = k.GetNamespace() + } + dSrcNamespace := obj.Spec.SourceRef.Namespace + if dSrcNamespace == "" { + dSrcNamespace = obj.GetNamespace() + } + if k.Spec.SourceRef.Name == obj.Spec.SourceRef.Name && - k.Spec.SourceRef.Namespace == obj.Spec.SourceRef.Namespace && + srcNamespace == dSrcNamespace && k.Spec.SourceRef.Kind == obj.Spec.SourceRef.Kind && - source.GetArtifact().Revision != k.Status.LastAppliedRevision { + !source.GetArtifact().HasRevision(k.Status.LastAppliedRevision) { return fmt.Errorf("dependency '%s' revision is not up to date", dName) } } @@ -564,20 +642,23 @@ func (r *KustomizationReconciler) generate(obj unstructured.Unstructured, func (r *KustomizationReconciler) build(ctx context.Context, obj *kustomizev1.Kustomization, u unstructured.Unstructured, workDir, dirPath string) ([]byte, error) { - dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj) + dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj, r.TokenCache) if err != nil { return nil, err } defer cleanup() - // Import decryption keys + // Import keys and static credentials for decryption. if err := dec.ImportKeys(ctx); err != nil { return nil, err } + // Set options for secret-less authentication with cloud providers for decryption. + dec.SetAuthOptions(ctx) + // Decrypt Kustomize EnvSources files before build - if err = dec.DecryptEnvSources(dirPath); err != nil { - return nil, fmt.Errorf("error decrypting env sources: %w", err) + if err = dec.DecryptSources(dirPath); err != nil { + return nil, fmt.Errorf("error decrypting sources: %w", err) } m, err := generator.SecureBuild(workDir, dirPath, !r.NoRemoteBases) @@ -608,9 +689,10 @@ func (r *KustomizationReconciler) build(ctx context.Context, // run variable substitutions if obj.Spec.PostBuild != nil { - outRes, err := generator.SubstituteVariables(ctx, r.Client, u, res, false) + outRes, err := generator.SubstituteVariables(ctx, r.Client, u, res, + generator.SubstituteWithStrict(r.StrictSubstitutions)) if err != nil { - return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) + return nil, fmt.Errorf("post build failed for '%s': %w", res.GetName(), err) } if outRes != nil { @@ -634,22 +716,66 @@ func (r *KustomizationReconciler) apply(ctx context.Context, manager *ssa.ResourceManager, obj *kustomizev1.Kustomization, revision string, + originRevision string, objects []*unstructured.Unstructured) (bool, *ssa.ChangeSet, error) { log := ctrl.LoggerFrom(ctx) - if err := ssa.SetNativeKindsDefaults(objects); err != nil { + if err := normalize.UnstructuredList(objects); err != nil { return false, nil, err } + if cmeta := obj.Spec.CommonMetadata; cmeta != nil { + ssautil.SetCommonMetadata(objects, cmeta.Labels, cmeta.Annotations) + } + applyOpts := ssa.DefaultApplyOptions() applyOpts.Force = obj.Spec.Force applyOpts.ExclusionSelector = map[string]string{ fmt.Sprintf("%s/reconcile", kustomizev1.GroupVersion.Group): kustomizev1.DisabledValue, + fmt.Sprintf("%s/ssa", kustomizev1.GroupVersion.Group): kustomizev1.IgnoreValue, + } + applyOpts.IfNotPresentSelector = map[string]string{ + fmt.Sprintf("%s/ssa", kustomizev1.GroupVersion.Group): kustomizev1.IfNotPresentValue, } applyOpts.ForceSelector = map[string]string{ fmt.Sprintf("%s/force", kustomizev1.GroupVersion.Group): kustomizev1.EnabledValue, } + fieldManagers := []ssa.FieldManager{ + { + // to undo changes made with 'kubectl apply --server-side --force-conflicts' + Name: "kubectl", + OperationType: metav1.ManagedFieldsOperationApply, + }, + { + // to undo changes made with 'kubectl apply' + Name: "kubectl", + OperationType: metav1.ManagedFieldsOperationUpdate, + }, + { + // to undo changes made with 'kubectl apply' + Name: "before-first-apply", + OperationType: metav1.ManagedFieldsOperationUpdate, + }, + { + // to undo changes made by the controller before SSA + Name: r.ControllerName, + OperationType: metav1.ManagedFieldsOperationUpdate, + }, + } + + for _, fieldManager := range r.DisallowedFieldManagers { + fieldManagers = append(fieldManagers, ssa.FieldManager{ + Name: fieldManager, + OperationType: metav1.ManagedFieldsOperationApply, + }) + // to undo changes made by the controller before SSA + fieldManagers = append(fieldManagers, ssa.FieldManager{ + Name: fieldManager, + OperationType: metav1.ManagedFieldsOperationUpdate, + }) + } + applyOpts.Cleanup = ssa.ApplyCleanupOptions{ Annotations: []string{ // remove the kubectl annotation @@ -662,28 +788,7 @@ func (r *KustomizationReconciler) apply(ctx context.Context, // remove deprecated fluxcd.io labels "fluxcd.io/sync-gc-mark", }, - FieldManagers: []ssa.FieldManager{ - { - // to undo changes made with 'kubectl apply --server-side --force-conflicts' - Name: "kubectl", - OperationType: metav1.ManagedFieldsOperationApply, - }, - { - // to undo changes made with 'kubectl apply' - Name: "kubectl", - OperationType: metav1.ManagedFieldsOperationUpdate, - }, - { - // to undo changes made with 'kubectl apply' - Name: "before-first-apply", - OperationType: metav1.ManagedFieldsOperationUpdate, - }, - { - // to undo changes made by the controller before SSA - Name: r.ControllerName, - OperationType: metav1.ManagedFieldsOperationUpdate, - }, - }, + FieldManagers: fieldManagers, Exclusions: map[string]string{ fmt.Sprintf("%s/ssa", kustomizev1.GroupVersion.Group): kustomizev1.MergeValue, }, @@ -693,7 +798,7 @@ func (r *KustomizationReconciler) apply(ctx context.Context, var defStage []*unstructured.Unstructured // contains only Kubernetes Class types e.g.: RuntimeClass, PriorityClass, - // StorageClas, VolumeSnapshotClass, IngressClass, GatewayClass, ClusterClass, etc + // StorageClass, VolumeSnapshotClass, IngressClass, GatewayClass, ClusterClass, etc var classStage []*unstructured.Unstructured // contains all objects except for CRDs, Namespaces and Class type objects @@ -706,11 +811,11 @@ func (r *KustomizationReconciler) apply(ctx context.Context, if decryptor.IsEncryptedSecret(u) { return false, nil, fmt.Errorf("%s is SOPS encrypted, configuring decryption is required for this secret to be reconciled", - ssa.FmtUnstructured(u)) + ssautil.FmtUnstructured(u)) } switch { - case ssa.IsClusterDefinition(u): + case ssautil.IsClusterDefinition(u): defStage = append(defStage, u) case strings.HasSuffix(u.GetKind(), "Class"): classStage = append(classStage, u) @@ -728,12 +833,17 @@ func (r *KustomizationReconciler) apply(ctx context.Context, if err != nil { return false, nil, err } - resultSet.Append(changeSet.Entries) if changeSet != nil && len(changeSet.Entries) > 0 { - log.Info("server-side apply for cluster definitions completed", "output", changeSet.ToMap()) + resultSet.Append(changeSet.Entries) + + if r.GroupChangeLog { + log.Info("server-side apply for cluster definitions completed", "output", changeSet.ToGroupedMap()) + } else { + log.Info("server-side apply for cluster definitions completed", "output", changeSet.ToMap()) + } for _, change := range changeSet.Entries { - if change.Action != string(ssa.UnchangedAction) { + if HasChanged(change.Action) { changeSetLog.WriteString(change.String() + "\n") } } @@ -753,12 +863,17 @@ func (r *KustomizationReconciler) apply(ctx context.Context, if err != nil { return false, nil, err } - resultSet.Append(changeSet.Entries) if changeSet != nil && len(changeSet.Entries) > 0 { - log.Info("server-side apply for cluster class types completed", "output", changeSet.ToMap()) + resultSet.Append(changeSet.Entries) + + if r.GroupChangeLog { + log.Info("server-side apply for cluster definitions completed", "output", changeSet.ToGroupedMap()) + } else { + log.Info("server-side apply for cluster class types completed", "output", changeSet.ToMap()) + } for _, change := range changeSet.Entries { - if change.Action != string(ssa.UnchangedAction) { + if HasChanged(change.Action) { changeSetLog.WriteString(change.String() + "\n") } } @@ -779,12 +894,17 @@ func (r *KustomizationReconciler) apply(ctx context.Context, if err != nil { return false, nil, fmt.Errorf("%w\n%s", err, changeSetLog.String()) } - resultSet.Append(changeSet.Entries) if changeSet != nil && len(changeSet.Entries) > 0 { - log.Info("server-side apply completed", "output", changeSet.ToMap(), "revision", revision) + resultSet.Append(changeSet.Entries) + + if r.GroupChangeLog { + log.Info("server-side apply for cluster definitions completed", "output", changeSet.ToGroupedMap()) + } else { + log.Info("server-side apply completed", "output", changeSet.ToMap(), "revision", revision) + } for _, change := range changeSet.Entries { - if change.Action != string(ssa.UnchangedAction) { + if HasChanged(change.Action) { changeSetLog.WriteString(change.String() + "\n") } } @@ -794,7 +914,7 @@ func (r *KustomizationReconciler) apply(ctx context.Context, // emit event only if the server-side apply resulted in changes applyLog := strings.TrimSuffix(changeSetLog.String(), "\n") if applyLog != "" { - r.event(obj, revision, eventv1.EventSeverityInfo, applyLog, nil) + r.event(obj, revision, originRevision, eventv1.EventSeverityInfo, applyLog, nil) } return applyLog != "", resultSet, nil @@ -805,11 +925,12 @@ func (r *KustomizationReconciler) checkHealth(ctx context.Context, patcher *patch.SerialPatcher, obj *kustomizev1.Kustomization, revision string, + originRevision string, isNewRevision bool, drifted bool, objects object.ObjMetadataSet) error { if len(obj.Spec.HealthChecks) == 0 && !obj.Spec.Wait { - conditions.Delete(obj, kustomizev1.HealthyCondition) + conditions.Delete(obj, meta.HealthyCondition) return nil } @@ -823,7 +944,7 @@ func (r *KustomizationReconciler) checkHealth(ctx context.Context, } if len(objects) == 0 { - conditions.Delete(obj, kustomizev1.HealthyCondition) + conditions.Delete(obj, meta.HealthyCondition) return nil } @@ -839,35 +960,36 @@ func (r *KustomizationReconciler) checkHealth(ctx context.Context, } // Find the previous health check result. - wasHealthy := apimeta.IsStatusConditionTrue(obj.Status.Conditions, kustomizev1.HealthyCondition) + wasHealthy := apimeta.IsStatusConditionTrue(obj.Status.Conditions, meta.HealthyCondition) // Update status with the reconciliation progress. message := fmt.Sprintf("Running health checks for revision %s with a timeout of %s", revision, obj.GetTimeout().String()) - conditions.MarkReconciling(obj, meta.ProgressingReason, message) - conditions.MarkUnknown(obj, kustomizev1.HealthyCondition, meta.ProgressingReason, message) + conditions.MarkReconciling(obj, meta.ProgressingReason, "%s", message) + conditions.MarkUnknown(obj, meta.HealthyCondition, meta.ProgressingReason, "%s", message) if err := r.patch(ctx, obj, patcher); err != nil { - return fmt.Errorf("unable to update the healthy status to progressing, error: %w", err) + return fmt.Errorf("unable to update the healthy status to progressing: %w", err) } // Check the health with a default timeout of 30sec shorter than the reconciliation interval. if err := manager.WaitForSet(toCheck, ssa.WaitOptions{ Interval: 5 * time.Second, Timeout: obj.GetTimeout(), + FailFast: r.FailFast, }); err != nil { - conditions.MarkFalse(obj, meta.ReadyCondition, kustomizev1.HealthCheckFailedReason, err.Error()) - conditions.MarkFalse(obj, kustomizev1.HealthyCondition, kustomizev1.HealthCheckFailedReason, err.Error()) - return fmt.Errorf("Health check failed after %s: %w", time.Since(checkStart).String(), err) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.HealthCheckFailedReason, "%s", err) + conditions.MarkFalse(obj, meta.HealthyCondition, meta.HealthCheckFailedReason, "%s", err) + return fmt.Errorf("health check failed after %s: %w", time.Since(checkStart).String(), err) } // Emit recovery event if the previous health check failed. msg := fmt.Sprintf("Health check passed in %s", time.Since(checkStart).String()) if !wasHealthy || (isNewRevision && drifted) { - r.event(obj, revision, eventv1.EventSeverityInfo, msg, nil) + r.event(obj, revision, originRevision, eventv1.EventSeverityInfo, msg, nil) } - conditions.MarkTrue(obj, kustomizev1.HealthyCondition, meta.SucceededReason, msg) + conditions.MarkTrue(obj, meta.HealthyCondition, meta.SucceededReason, "%s", msg) if err := r.patch(ctx, obj, patcher); err != nil { - return fmt.Errorf("unable to update the healthy status to progressing, error: %w", err) + return fmt.Errorf("unable to update the healthy status to progressing: %w", err) } return nil @@ -877,6 +999,7 @@ func (r *KustomizationReconciler) prune(ctx context.Context, manager *ssa.ResourceManager, obj *kustomizev1.Kustomization, revision string, + originRevision string, objects []*unstructured.Unstructured) (bool, error) { if !obj.Spec.Prune { return false, nil @@ -901,34 +1024,73 @@ func (r *KustomizationReconciler) prune(ctx context.Context, // emit event only if the prune operation resulted in changes if changeSet != nil && len(changeSet.Entries) > 0 { log.Info(fmt.Sprintf("garbage collection completed: %s", changeSet.String())) - r.event(obj, revision, eventv1.EventSeverityInfo, changeSet.String(), nil) + r.event(obj, revision, originRevision, eventv1.EventSeverityInfo, changeSet.String(), nil) return true, nil } return false, nil } +// finalizerShouldDeleteResources determines if resources should be deleted +// based on the object's inventory and deletion policy. +// A suspended Kustomization or one without an inventory will not delete resources. +func finalizerShouldDeleteResources(obj *kustomizev1.Kustomization) bool { + if obj.Spec.Suspend { + return false + } + + if obj.Status.Inventory == nil || len(obj.Status.Inventory.Entries) == 0 { + return false + } + + switch obj.GetDeletionPolicy() { + case kustomizev1.DeletionPolicyMirrorPrune: + return obj.Spec.Prune + case kustomizev1.DeletionPolicyDelete: + return true + case kustomizev1.DeletionPolicyWaitForTermination: + return true + default: + return false + } +} + +// finalize handles the finalization logic for a Kustomization resource during its deletion process. +// Managed resources are pruned based on the deletion policy and suspended state of the Kustomization. +// When the policy is set to WaitForTermination, the function blocks and waits for the resources +// to be terminated by the Kubernetes Garbage Collector for the specified timeout duration. +// If the service account used for impersonation is no longer available or if a timeout occurs +// while waiting for resources to be terminated, an error is logged and the finalizer is removed. func (r *KustomizationReconciler) finalize(ctx context.Context, obj *kustomizev1.Kustomization) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) - if obj.Spec.Prune && - !obj.Spec.Suspend && - obj.Status.Inventory != nil && - obj.Status.Inventory.Entries != nil { + if finalizerShouldDeleteResources(obj) { objects, _ := inventory.List(obj.Status.Inventory) - impersonation := runtimeClient.NewImpersonator( - r.Client, - r.StatusPoller, - r.PollingOpts, - obj.Spec.KubeConfig, - r.KubeConfigOpts, - r.DefaultServiceAccount, - obj.Spec.ServiceAccountName, - obj.GetNamespace(), - ) + var impersonatorOpts []runtimeClient.ImpersonatorOption + var mustImpersonate bool + if r.DefaultServiceAccount != "" || obj.Spec.ServiceAccountName != "" { + mustImpersonate = true + impersonatorOpts = append(impersonatorOpts, + runtimeClient.WithServiceAccount(r.DefaultServiceAccount, obj.Spec.ServiceAccountName, obj.GetNamespace())) + } + if obj.Spec.KubeConfig != nil { + mustImpersonate = true + impersonatorOpts = append(impersonatorOpts, + runtimeClient.WithKubeConfig(obj.Spec.KubeConfig, r.KubeConfigOpts, obj.GetNamespace())) + } + if r.ClusterReader != nil { + impersonatorOpts = append(impersonatorOpts, runtimeClient.WithPolling(r.ClusterReader)) + } + impersonation := runtimeClient.NewImpersonator(r.Client, impersonatorOpts...) if impersonation.CanImpersonate(ctx) { - kubeClient, _, err := impersonation.GetClient(ctx) + var kubeClient client.Client + var err error + if mustImpersonate { + kubeClient, _, err = impersonation.GetClient(ctx) + } else { + kubeClient = r.Client + } if err != nil { return ctrl.Result{}, err } @@ -949,40 +1111,62 @@ func (r *KustomizationReconciler) finalize(ctx context.Context, changeSet, err := resourceManager.DeleteAll(ctx, objects, opts) if err != nil { - r.event(obj, obj.Status.LastAppliedRevision, eventv1.EventSeverityError, "pruning for deleted resource failed", nil) + r.event(obj, obj.Status.LastAppliedRevision, obj.Status.LastAppliedOriginRevision, eventv1.EventSeverityError, "pruning for deleted resource failed", nil) // Return the error so we retry the failed garbage collection return ctrl.Result{}, err } if changeSet != nil && len(changeSet.Entries) > 0 { - r.event(obj, obj.Status.LastAppliedRevision, eventv1.EventSeverityInfo, changeSet.String(), nil) + // Emit event with the resources marked for deletion. + r.event(obj, obj.Status.LastAppliedRevision, obj.Status.LastAppliedOriginRevision, eventv1.EventSeverityInfo, changeSet.String(), nil) + + // Wait for the resources marked for deletion to be terminated. + if obj.GetDeletionPolicy() == kustomizev1.DeletionPolicyWaitForTermination { + if err := resourceManager.WaitForSetTermination(changeSet, ssa.WaitOptions{ + Interval: 2 * time.Second, + Timeout: obj.GetTimeout(), + }); err != nil { + // Emit an event and log the error if a timeout occurs. + msg := "failed to wait for resources termination" + log.Error(err, msg) + r.event(obj, obj.Status.LastAppliedRevision, obj.Status.LastAppliedOriginRevision, eventv1.EventSeverityError, msg, nil) + } + } } } else { // when the account to impersonate is gone, log the stale objects and continue with the finalization - msg := fmt.Sprintf("unable to prune objects: \n%s", ssa.FmtUnstructuredList(objects)) + msg := fmt.Sprintf("unable to prune objects: \n%s", ssautil.FmtUnstructuredList(objects)) log.Error(fmt.Errorf("skiping pruning, failed to find account to impersonate"), msg) - r.event(obj, obj.Status.LastAppliedRevision, eventv1.EventSeverityError, msg, nil) + r.event(obj, obj.Status.LastAppliedRevision, obj.Status.LastAppliedOriginRevision, eventv1.EventSeverityError, msg, nil) } } // Remove our finalizer from the list and update it controllerutil.RemoveFinalizer(obj, kustomizev1.KustomizationFinalizer) + + // Cleanup caches. + for _, op := range intcache.AllOperations { + r.TokenCache.DeleteEventsForObject(kustomizev1.KustomizationKind, obj.GetName(), obj.GetNamespace(), op) + } + // Stop reconciliation as the object is being deleted return ctrl.Result{}, nil } func (r *KustomizationReconciler) event(obj *kustomizev1.Kustomization, - revision, severity, msg string, + revision, originRevision, severity, msg string, metadata map[string]string) { if metadata == nil { metadata = map[string]string{} } if revision != "" { - metadata[kustomizev1.GroupVersion.Group+"/revision"] = revision + metadata[kustomizev1.GroupVersion.Group+"/"+eventv1.MetaRevisionKey] = revision + } + if originRevision != "" { + metadata[kustomizev1.GroupVersion.Group+"/"+eventv1.MetaOriginRevisionKey] = originRevision } reason := severity - conditions.GetReason(obj, meta.ReadyCondition) if r := conditions.GetReason(obj, meta.ReadyCondition); r != "" { reason = r } @@ -1015,7 +1199,7 @@ func (r *KustomizationReconciler) finalizeStatus(ctx context.Context, if conditions.IsFalse(obj, meta.ReadyCondition) && conditions.Has(obj, meta.ReconcilingCondition) { rc := conditions.Get(obj, meta.ReconcilingCondition) - rc.Reason = kustomizev1.ProgressingWithRetryReason + rc.Reason = meta.ProgressingWithRetryReason conditions.Set(obj, rc) } @@ -1030,7 +1214,7 @@ func (r *KustomizationReconciler) patch(ctx context.Context, // Configure the runtime patcher. patchOpts := []patch.Option{} ownedConditions := []string{ - kustomizev1.HealthyCondition, + meta.HealthyCondition, meta.ReadyCondition, meta.ReconcilingCondition, meta.StalledCondition, @@ -1054,3 +1238,37 @@ func (r *KustomizationReconciler) patch(ctx context.Context, return nil } + +// getClientAndPoller creates a status poller with the custom status readers +// from CEL expressions and the custom job status reader, and returns the +// Kubernetes client of the controller and the status poller. +// Should be used for reconciliations that are not configured to use +// ServiceAccount impersonation or kubeconfig. +func (r *KustomizationReconciler) getClientAndPoller( + readerCtors []func(apimeta.RESTMapper) engine.StatusReader, +) (client.Client, *polling.StatusPoller) { + + readers := make([]engine.StatusReader, 0, 1+len(readerCtors)) + readers = append(readers, statusreaders.NewCustomJobStatusReader(r.Mapper)) + for _, ctor := range readerCtors { + readers = append(readers, ctor(r.Mapper)) + } + + poller := polling.NewStatusPoller(r.Client, r.Mapper, polling.Options{ + CustomStatusReaders: readers, + ClusterReaderFactory: r.ClusterReader, + }) + + return r.Client, poller +} + +// getOriginRevision returns the origin revision of the source artifact, +// or the empty string if it's not present, or if the artifact itself +// is not present. +func getOriginRevision(src sourcev1.Source) string { + a := src.GetArtifact() + if a == nil { + return "" + } + return a.Metadata[OCIArtifactOriginRevisionAnnotation] +} diff --git a/internal/controller/kustomization_controller_test.go b/internal/controller/kustomization_controller_test.go new file mode 100644 index 000000000..e1b1cebbe --- /dev/null +++ b/internal/controller/kustomization_controller_test.go @@ -0,0 +1,138 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" +) + +func TestKustomizationReconciler_StagedApply(t *testing.T) { + g := NewWithT(t) + + namespaceName := "kust-" + randStringRunes(5) + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, + } + g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred()) + t.Cleanup(func() { + g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred()) + }) + + err := createKubeConfigSecret(namespaceName) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + artifactName := "val-" + randStringRunes(5) + artifactChecksum, err := testServer.ArtifactFromDir("testdata/crds", artifactName) + g.Expect(err).ToNot(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("val-%s", randStringRunes(5)), + Namespace: namespaceName, + } + + err = applyGitRepository(repositoryName, artifactName, "main/"+artifactChecksum) + g.Expect(err).NotTo(HaveOccurred()) + + kustomization := &kustomizev1.Kustomization{} + kustomization.Name = "test-kust" + kustomization.Namespace = namespaceName + kustomization.Spec = kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: 10 * time.Minute}, + Prune: true, + Path: "./", + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + g.Eventually(func() bool { + var obj kustomizev1.Kustomization + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &obj) + return isReconcileSuccess(&obj) && obj.Status.LastAttemptedRevision == "main/"+artifactChecksum + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed()) + + g.Eventually(func() bool { + var obj kustomizev1.Kustomization + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &obj) + return errors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) +} + +func TestKustomizationReconciler_deleteBeforeFinalizer(t *testing.T) { + g := NewWithT(t) + + namespaceName := "kust-" + randStringRunes(5) + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: namespaceName}, + } + g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred()) + t.Cleanup(func() { + g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred()) + }) + + kustomization := &kustomizev1.Kustomization{} + kustomization.Name = "test-kust" + kustomization.Namespace = namespaceName + kustomization.Spec = kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: interval}, + Prune: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: "Bucket", + Name: "foo", + }, + } + // Add a test finalizer to prevent the object from getting deleted. + kustomization.SetFinalizers([]string{"test-finalizer"}) + g.Expect(k8sClient.Create(ctx, kustomization)).NotTo(HaveOccurred()) + // Add deletion timestamp by deleting the object. + g.Expect(k8sClient.Delete(ctx, kustomization)).NotTo(HaveOccurred()) + + r := &KustomizationReconciler{ + Client: k8sClient, + EventRecorder: record.NewFakeRecorder(32), + } + // NOTE: Only a real API server responds with an error in this scenario. + _, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(kustomization)}) + g.Expect(err).NotTo(HaveOccurred()) +} diff --git a/controllers/kustomization_decryptor_test.go b/internal/controller/kustomization_decryptor_test.go similarity index 57% rename from controllers/kustomization_decryptor_test.go rename to internal/controller/kustomization_decryptor_test.go index d6b0bb85a..1998e1e99 100644 --- a/controllers/kustomization_decryptor_test.go +++ b/internal/controller/kustomization_decryptor_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -24,15 +24,16 @@ import ( "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/hashicorp/vault/api" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Decryptor(t *testing.T) { @@ -42,18 +43,18 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) { g.Expect(err).NotTo(HaveOccurred(), "failed to create vault client") // create a master key on the vault transit engine - path, data := "sops/keys/firstkey", map[string]interface{}{"type": "rsa-4096"} + path, data := "sops/keys/vault", map[string]interface{}{"type": "rsa-4096"} _, err = cli.Logical().Write(path, data) g.Expect(err).NotTo(HaveOccurred(), "failed to write key") // encrypt the testdata vault secret - cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/firstkey", "--encrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/secret.vault.yaml") + cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/vault", "--encrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/algorithms/vault.yaml") err = cmd.Run() g.Expect(err).NotTo(HaveOccurred(), "failed to encrypt file") // defer the testdata vault secret decryption, to leave a clean testdata vault secret defer func() { - cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/firstkey", "--decrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/secret.vault.yaml") + cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/firstkey", "--decrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/algorithms/vault.yaml") err = cmd.Run() }() @@ -69,36 +70,23 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) { artifactChecksum, err := testServer.ArtifactFromDir("testdata/sops", artifactName) g.Expect(err).ToNot(HaveOccurred()) - overlayArtifactName := "sops-" + randStringRunes(5) - overlayChecksum, err := testServer.ArtifactFromDir("testdata/test-dotenv", overlayArtifactName) - g.Expect(err).ToNot(HaveOccurred()) - repositoryName := types.NamespacedName{ Name: fmt.Sprintf("sops-%s", randStringRunes(5)), Namespace: id, } - overlayRepositoryName := types.NamespacedName{ - Name: fmt.Sprintf("sops-%s", randStringRunes(5)), - Namespace: id, - } - err = applyGitRepository(repositoryName, artifactName, "main/"+artifactChecksum) g.Expect(err).NotTo(HaveOccurred()) - err = applyGitRepository(overlayRepositoryName, overlayArtifactName, "main/"+overlayChecksum) - g.Expect(err).NotTo(HaveOccurred()) - - pgpKey, err := os.ReadFile("testdata/sops/pgp.asc") + pgpKey, err := os.ReadFile("testdata/sops/keys/pgp.asc") g.Expect(err).ToNot(HaveOccurred()) - ageKey, err := os.ReadFile("testdata/sops/age.txt") + ageKey, err := os.ReadFile("testdata/sops/keys/age.txt") g.Expect(err).ToNot(HaveOccurred()) sopsSecretKey := types.NamespacedName{ Name: "sops-" + randStringRunes(5), Namespace: id, } - sopsSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: sopsSecretKey.Name, @@ -152,60 +140,40 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) { return obj.Status.LastAppliedRevision == "main/"+artifactChecksum }, timeout, time.Second).Should(BeTrue()) - overlayKustomizationName := fmt.Sprintf("sops-%s", randStringRunes(5)) - overlayKs := kustomization.DeepCopy() - overlayKs.ResourceVersion = "" - overlayKs.Name = overlayKustomizationName - overlayKs.Spec.SourceRef.Name = overlayRepositoryName.Name - overlayKs.Spec.SourceRef.Namespace = overlayRepositoryName.Namespace - overlayKs.Spec.Path = "./testdata/test-dotenv/overlays" - - g.Expect(k8sClient.Create(context.TODO(), overlayKs)).To(Succeed()) - - g.Eventually(func() bool { - var obj kustomizev1.Kustomization - _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(overlayKs), &obj) - return obj.Status.LastAppliedRevision == "main/"+overlayChecksum - }, timeout, time.Second).Should(BeTrue()) - t.Run("decrypts SOPS secrets", func(t *testing.T) { g := NewWithT(t) - var pgpSecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-pgp", Namespace: id}, &pgpSecret)).To(Succeed()) - g.Expect(pgpSecret.Data["secret"]).To(Equal([]byte(`my-sops-pgp-secret`))) - - var ageSecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-age", Namespace: id}, &ageSecret)).To(Succeed()) - g.Expect(ageSecret.Data["secret"]).To(Equal([]byte(`my-sops-age-secret`))) - - var daySecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-day", Namespace: id}, &daySecret)).To(Succeed()) - g.Expect(string(daySecret.Data["secret"])).To(Equal("day=Tuesday\n")) - - var yearSecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year", Namespace: id}, &yearSecret)).To(Succeed()) - g.Expect(string(yearSecret.Data["year"])).To(Equal("2017")) - - var unencryptedSecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "unencrypted-sops-year", Namespace: id}, &unencryptedSecret)).To(Succeed()) - g.Expect(string(unencryptedSecret.Data["year"])).To(Equal("2021")) - - var year1Secret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year1", Namespace: id}, &year1Secret)).To(Succeed()) - g.Expect(string(year1Secret.Data["year"])).To(Equal("year1")) - - var year2Secret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year2", Namespace: id}, &year2Secret)).To(Succeed()) - g.Expect(string(year2Secret.Data["year"])).To(Equal("year2")) - - var encodedSecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-month", Namespace: id}, &encodedSecret)).To(Succeed()) - g.Expect(string(encodedSecret.Data["month.yaml"])).To(Equal("month: May\n")) - - var hcvaultSecret corev1.Secret - g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-hcvault", Namespace: id}, &hcvaultSecret)).To(Succeed()) - g.Expect(string(hcvaultSecret.Data["secret"])).To(Equal("my-sops-vault-secret\n")) + secretNames := []string{ + "sops-algo-age", + "sops-algo-pgp", + "sops-algo-vault", + "sops-component", + "sops-envs-secret", + "sops-files-secret", + "sops-inside-secret", + "sops-remote-secret", + } + for _, name := range secretNames { + var secret corev1.Secret + g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: id}, &secret)).To(Succeed()) + g.Expect(string(secret.Data["key"])).To(Equal("value"), fmt.Sprintf("failed on secret %s", name)) + } + + configMapNames := []string{ + "sops-envs-configmap", + "sops-files-configmap", + "sops-remote-configmap", + } + for _, name := range configMapNames { + var configMap corev1.ConfigMap + g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: id}, &configMap)).To(Succeed()) + g.Expect(string(configMap.Data["key"])).To(Equal("value"), fmt.Sprintf("failed on configmap %s", name)) + } + + var patchedSecret corev1.Secret + g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-patches-secret", Namespace: id}, &patchedSecret)).To(Succeed()) + g.Expect(string(patchedSecret.Data["key"])).To(Equal("merge1")) + g.Expect(string(patchedSecret.Data["merge2"])).To(Equal("merge2")) }) t.Run("does not emit change events for identical secrets", func(t *testing.T) { diff --git a/internal/controller/kustomization_deletion_policy_test.go b/internal/controller/kustomization_deletion_policy_test.go new file mode 100644 index 000000000..e1293bc44 --- /dev/null +++ b/internal/controller/kustomization_deletion_policy_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" +) + +func TestKustomizationReconciler_DeletionPolicyDelete(t *testing.T) { + tests := []struct { + name string + prune bool + deletionPolicy string + wantDelete bool + }{ + { + name: "should delete when deletionPolicy overrides pruning disabled", + prune: false, + deletionPolicy: kustomizev1.DeletionPolicyDelete, + wantDelete: true, + }, + { + name: "should delete and wait when deletionPolicy overrides pruning disabled", + prune: false, + deletionPolicy: kustomizev1.DeletionPolicyWaitForTermination, + wantDelete: true, + }, + { + name: "should delete when deletionPolicy mirrors prune and pruning enabled", + prune: true, + deletionPolicy: kustomizev1.DeletionPolicyMirrorPrune, + wantDelete: true, + }, + { + name: "should orphan when deletionPolicy overrides pruning enabled", + prune: true, + deletionPolicy: kustomizev1.DeletionPolicyOrphan, + wantDelete: false, + }, + { + name: "should orphan when deletionPolicy mirrors prune and pruning disabled", + prune: false, + deletionPolicy: kustomizev1.DeletionPolicyMirrorPrune, + wantDelete: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + id := "gc-" + randStringRunes(5) + revision := "v1.0.0" + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string, data string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: + key: "%[2]s" +`, name, data), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id, id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("gc-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("gc-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + TargetNamespace: id, + Prune: tt.prune, + DeletionPolicy: tt.deletionPolicy, + Timeout: &metav1.Duration{Duration: 5 * time.Second}, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + resultK := &kustomizev1.Kustomization{} + resultConfig := &corev1.ConfigMap{} + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return resultK.Status.LastAppliedRevision == revision + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: id, Namespace: id}, resultConfig)).Should(Succeed()) + + g.Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed()) + g.Eventually(func() bool { + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), kustomization) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + + if tt.wantDelete { + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(resultConfig), resultConfig) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } else { + g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(resultConfig), resultConfig)).Should(Succeed()) + } + + }) + } +} diff --git a/controllers/kustomization_dependson_test.go b/internal/controller/kustomization_dependson_test.go similarity index 93% rename from controllers/kustomization_dependson_test.go rename to internal/controller/kustomization_dependson_test.go index de40ed506..100575e34 100644 --- a/controllers/kustomization_dependson_test.go +++ b/internal/controller/kustomization_dependson_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -24,14 +24,14 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_DependsOn(t *testing.T) { @@ -148,7 +148,7 @@ spec: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return ready.Reason == kustomizev1.ArtifactFailedReason + return ready.Reason == meta.ArtifactFailedReason }, timeout, time.Second).Should(BeTrue()) }) @@ -160,7 +160,7 @@ spec: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return ready.Reason == kustomizev1.ReconciliationSucceededReason + return ready.Reason == meta.ReconciliationSucceededReason }, timeout, time.Second).Should(BeTrue()) }) @@ -180,7 +180,7 @@ spec: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return ready.Reason == kustomizev1.DependencyNotReadyReason + return ready.Reason == meta.DependencyNotReadyReason }, timeout, time.Second).Should(BeTrue()) }) } diff --git a/internal/controller/kustomization_disallowed_managers_test.go b/internal/controller/kustomization_disallowed_managers_test.go new file mode 100644 index 000000000..dffb2e5a3 --- /dev/null +++ b/internal/controller/kustomization_disallowed_managers_test.go @@ -0,0 +1,156 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" +) + +func TestKustomizationReconciler_DisallowedManagers(t *testing.T) { + g := NewWithT(t) + id := "disallowed-managers-" + randStringRunes(5) + revision := "v1.0.0" + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string, data string) []testserver.File { + return []testserver.File{ + { + Name: "configmap.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: + key: %[2]s +`, name, data), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id, randStringRunes(5))) + g.Expect(err).NotTo(HaveOccurred(), "failed to create artifact from files") + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("disallowed-managers-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("disallowed-managers-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + HealthChecks: []meta.NamespacedObjectKindReference{ + { + APIVersion: "v1", + Kind: "ConfigMap", + Name: id, + Namespace: id, + }, + }, + TargetNamespace: id, + Force: false, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + resultK := &kustomizev1.Kustomization{} + initialConfigMap := &corev1.ConfigMap{} + badConfigMap := &corev1.ConfigMap{} + fixedConfigMap := &corev1.ConfigMap{} + + t.Run("creates configmap", func(t *testing.T) { + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return resultK.Status.LastAppliedRevision == revision + }, timeout, time.Second).Should(BeTrue()) + logStatus(t, resultK) + + kstatusCheck.CheckErr(ctx, resultK) + g.Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: id, Namespace: id}, initialConfigMap)).Should(Succeed()) + g.Expect(initialConfigMap.Data).Should(HaveKey("key")) + }) + + t.Run("update configmap with new data", func(t *testing.T) { + configMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: id, + Namespace: id, + }, + } + err = k8sClient.Patch(context.Background(), &configMap, client.RawPatch(types.MergePatchType, []byte(`{"data":{"bad-key":"overridden field manager"}}`)), &client.PatchOptions{FieldManager: overrideManagerName}) + g.Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Patch(context.Background(), &configMap, client.RawPatch(types.MergePatchType, []byte(`{"data":{"key2":"not overridden field manager"}}`)), &client.PatchOptions{FieldManager: "good-name"}) + g.Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(initialConfigMap), badConfigMap) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(badConfigMap.Data).Should(HaveKey("bad-key")) + g.Expect(badConfigMap.Data).Should(HaveKey("key2")) + }) + + t.Run("bad-key should be removed from the configmap", func(t *testing.T) { + reconciler.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: kustomizationKey, + }) + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(initialConfigMap), fixedConfigMap) + return g.Expect(fixedConfigMap.Data).ShouldNot(HaveKey("bad-key")) && g.Expect(fixedConfigMap.Data).Should(HaveKey("key2")) + }, timeout, time.Second).Should(BeTrue()) + }) +} diff --git a/controllers/kustomization_fetcher_test.go b/internal/controller/kustomization_fetcher_test.go similarity index 96% rename from controllers/kustomization_fetcher_test.go rename to internal/controller/kustomization_fetcher_test.go index 3bf0794a4..33ef80cf7 100644 --- a/controllers/kustomization_fetcher_test.go +++ b/internal/controller/kustomization_fetcher_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -25,14 +25,14 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_ArtifactDownload(t *testing.T) { diff --git a/controllers/kustomization_force_test.go b/internal/controller/kustomization_force_test.go similarity index 94% rename from controllers/kustomization_force_test.go rename to internal/controller/kustomization_force_test.go index c3f1d9986..4432217de 100644 --- a/controllers/kustomization_force_test.go +++ b/internal/controller/kustomization_force_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -22,16 +22,17 @@ import ( "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Force(t *testing.T) { @@ -143,7 +144,7 @@ stringData: events := getEvents(resultK.GetName(), map[string]string{"kustomize.toolkit.fluxcd.io/revision": revision}) g.Expect(len(events) > 0).To(BeTrue()) g.Expect(events[0].Type).To(BeIdenticalTo("Warning")) - g.Expect(events[0].Message).To(ContainSubstring("invalid, error: secret is immutable")) + g.Expect(events[0].Message).To(ContainSubstring("field is immutable")) }) }) @@ -168,6 +169,6 @@ stringData: kstatusCheck.CheckErr(ctx, resultK) - g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, kustomizev1.HealthyCondition)).To(BeTrue()) + g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.HealthyCondition)).To(BeTrue()) }) } diff --git a/controllers/kustomization_fuzzer_test.go b/internal/controller/kustomization_fuzzer_test.go similarity index 96% rename from controllers/kustomization_fuzzer_test.go rename to internal/controller/kustomization_fuzzer_test.go index d8f68364c..523f9e6a8 100644 --- a/controllers/kustomization_fuzzer_test.go +++ b/internal/controller/kustomization_fuzzer_test.go @@ -17,14 +17,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "archive/tar" "compress/gzip" "context" "crypto/sha1" - "crypto/sha256" "embed" "errors" "fmt" @@ -41,6 +40,7 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" "github.com/hashicorp/vault/api" + "github.com/opencontainers/go-digest" "github.com/ory/dockertest/v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -57,10 +57,11 @@ import ( "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/testenv" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" fuzz "github.com/AdaLogics/go-fuzz-headers" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var ( @@ -75,12 +76,12 @@ var ( debugMode = os.Getenv("DEBUG_TEST") != "" ) -const vaultVersion = "1.2.2" +const vaultVersion = "1.13.2" const defaultBinVersion = "1.24" //go:embed testdata/crd/*.yaml -//go:embed testdata/sops/pgp.asc -//go:embed testdata/sops/age.txt +//go:embed testdata/sops/keys/pgp.asc +//go:embed testdata/sops/keys/age.txt var testFiles embed.FS // FuzzControllers implements a fuzzer that targets the Kustomize controller. @@ -124,8 +125,9 @@ func Fuzz_Controllers(f *testing.F) { reconciler := &KustomizationReconciler{ ControllerName: controllerName, Client: testEnv, + Mapper: testEnv.GetRESTMapper(), } - if err := (reconciler).SetupWithManager(testEnv, KustomizationReconcilerOptions{MaxConcurrentReconciles: 1}); err != nil { + if err := (reconciler).SetupWithManager(ctx, testEnv, KustomizationReconcilerOptions{}); err != nil { panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err)) } }, func() error { @@ -181,11 +183,11 @@ func Fuzz_Controllers(f *testing.F) { if err != nil { return err } - pgpKey, err := testFiles.ReadFile("testdata/sops/pgp.asc") + pgpKey, err := testFiles.ReadFile("testdata/sops/keys/pgp.asc") if err != nil { return err } - ageKey, err := testFiles.ReadFile("testdata/sops/age.txt") + ageKey, err := testFiles.ReadFile("testdata/sops/keys/age.txt") if err != nil { return err } @@ -363,7 +365,7 @@ func createFiles(f *fuzz.ConsumeFuzzer, rootDir string) error { continue // some errors here are not permanent, so we can try again with different values } - err = os.MkdirAll(dirPath, 0o755) + err = os.MkdirAll(dirPath, 0o750) if err != nil { if noOfCreatedFiles > 0 { return nil @@ -432,7 +434,7 @@ func ensureDependencies() error { // as it is being consumed directly from the embed.FS. embedDirs := []string{"testdata/crd"} for _, dir := range embedDirs { - err := os.MkdirAll(dir, 0o755) + err := os.MkdirAll(dir, 0o750) if err != nil { return fmt.Errorf("mkdir %s: %v", dir, err) } @@ -451,7 +453,7 @@ func ensureDependencies() error { return fmt.Errorf("reading embedded file %s: %v", fileName, err) } - os.WriteFile(fileName, data, 0o644) + os.WriteFile(fileName, data, 0o600) if err != nil { return fmt.Errorf("writing %s: %v", fileName, err) } @@ -598,7 +600,7 @@ func applyGitRepository(objKey client.ObjectKey, artifactName string, revision s } b, _ := os.ReadFile(filepath.Join(testServer.Root(), artifactName)) - checksum := fmt.Sprintf("%x", sha256.Sum256(b)) + dig := digest.SHA256.FromBytes(b) url := fmt.Sprintf("%s/%s", testServer.URL(), artifactName) @@ -615,7 +617,7 @@ func applyGitRepository(objKey client.ObjectKey, artifactName string, revision s Path: url, URL: url, Revision: revision, - Checksum: checksum, + Digest: dig.String(), LastUpdateTime: metav1.Now(), }, } @@ -726,7 +728,7 @@ func createArtifact(artifactServer *testserver.ArtifactServer, fixture, path str return "", err } - if err := os.Chmod(f.Name(), 0644); err != nil { + if err := os.Chmod(f.Name(), 0o600); err != nil { return "", err } diff --git a/controllers/kustomization_impersonation_test.go b/internal/controller/kustomization_impersonation_test.go similarity index 94% rename from controllers/kustomization_impersonation_test.go rename to internal/controller/kustomization_impersonation_test.go index 0b1f45b91..3bd9b27ba 100644 --- a/controllers/kustomization_impersonation_test.go +++ b/internal/controller/kustomization_impersonation_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -22,10 +22,9 @@ import ( "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -34,6 +33,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Impersonation(t *testing.T) { @@ -118,7 +119,7 @@ data: return resultK.Status.LastAppliedRevision == revision }, timeout, time.Second).Should(BeTrue()) - g.Expect(readyCondition.Reason).To(Equal(kustomizev1.ReconciliationSucceededReason)) + g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationSucceededReason)) }) t.Run("fails to reconcile impersonating the default service account", func(t *testing.T) { @@ -130,7 +131,7 @@ data: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) readyCondition = apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return readyCondition.Reason == kustomizev1.ReconciliationFailedReason + return readyCondition.Reason == meta.ReconciliationFailedReason }, timeout, time.Second).Should(BeTrue()) g.Expect(readyCondition.Message).To(ContainSubstring("system:serviceaccount:%s:default", id)) @@ -186,7 +187,7 @@ data: return resultK.Status.LastAppliedRevision == revision }, timeout, time.Second).Should(BeTrue()) - g.Expect(readyCondition.Reason).To(Equal(kustomizev1.ReconciliationSucceededReason)) + g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationSucceededReason)) }) t.Run("can finalize impersonating service account", func(t *testing.T) { @@ -287,7 +288,7 @@ data: return apimeta.IsStatusConditionFalse(resultK.Status.Conditions, meta.ReadyCondition) }, timeout, time.Second).Should(BeTrue()) - g.Expect(readyCondition.Reason).To(Equal(kustomizev1.ReconciliationFailedReason)) + g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationFailedReason)) g.Expect(readyCondition.Message).To(ContainSubstring(`Secret "%s" not found`, secretName)) }) @@ -313,7 +314,7 @@ data: return resultK.Status.LastAppliedRevision == revision }, timeout, time.Second).Should(BeTrue()) - g.Expect(readyCondition.Reason).To(Equal(kustomizev1.ReconciliationSucceededReason)) + g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationSucceededReason)) }) } diff --git a/controllers/kustomization_indexers.go b/internal/controller/kustomization_indexers.go similarity index 66% rename from controllers/kustomization_indexers.go rename to internal/controller/kustomization_indexers.go index 0801b0800..e3c6ce276 100644 --- a/controllers/kustomization_indexers.go +++ b/internal/controller/kustomization_indexers.go @@ -14,51 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" "fmt" + "github.com/fluxcd/pkg/runtime/conditions" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/runtime/dependency" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) -func (r *KustomizationReconciler) requestsForRevisionChangeOf(indexKey string) func(obj client.Object) []reconcile.Request { - return func(obj client.Object) []reconcile.Request { +func (r *KustomizationReconciler) requestsForRevisionChangeOf(indexKey string) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + log := ctrl.LoggerFrom(ctx) repo, ok := obj.(interface { GetArtifact() *sourcev1.Artifact }) if !ok { - panic(fmt.Sprintf("Expected an object conformed with GetArtifact() method, but got a %T", obj)) + log.Error(fmt.Errorf("expected an object conformed with GetArtifact() method, but got a %T", obj), + "failed to get reconcile requests for revision change") + return nil } // If we do not have an artifact, we have no requests to make if repo.GetArtifact() == nil { return nil } - ctx := context.Background() var list kustomizev1.KustomizationList if err := r.List(ctx, &list, client.MatchingFields{ indexKey: client.ObjectKeyFromObject(obj).String(), }); err != nil { + log.Error(err, "failed to list objects for revision change") return nil } var dd []dependency.Dependent - for _, d := range list.Items { - // If the revision of the artifact equals to the last attempted revision, - // we should not make a request for this Kustomization - if repo.GetArtifact().Revision == d.Status.LastAttemptedRevision { + for i, d := range list.Items { + // If the Kustomization is ready and the revision of the artifact equals + // to the last attempted revision, we should not make a request for this Kustomization + if conditions.IsReady(&list.Items[i]) && repo.GetArtifact().HasRevision(d.Status.LastAttemptedRevision) { continue } dd = append(dd, d.DeepCopy()) } sorted, err := dependency.Sort(dd) if err != nil { + log.Error(err, "failed to sort dependencies for revision change") return nil } reqs := make([]reconcile.Request, len(sorted)) diff --git a/controllers/kustomization_inventory_test.go b/internal/controller/kustomization_inventory_test.go similarity index 97% rename from controllers/kustomization_inventory_test.go rename to internal/controller/kustomization_inventory_test.go index 1dc0438c0..b9c346d6a 100644 --- a/controllers/kustomization_inventory_test.go +++ b/internal/controller/kustomization_inventory_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -25,18 +25,18 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/cli-utils/pkg/object" + "github.com/fluxcd/cli-utils/pkg/object" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Inventory(t *testing.T) { diff --git a/internal/controller/kustomization_origin_revision_test.go b/internal/controller/kustomization_origin_revision_test.go new file mode 100644 index 000000000..6293fe8ec --- /dev/null +++ b/internal/controller/kustomization_origin_revision_test.go @@ -0,0 +1,125 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "testing" + "time" + + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + . "github.com/onsi/gomega" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" +) + +func TestKustomizationReconciler_OriginRevision(t *testing.T) { + g := NewWithT(t) + id := "force-" + randStringRunes(5) + revision := "v1.0.0" + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string, data string) []testserver.File { + return []testserver.File{ + { + Name: "secret.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: Secret +metadata: + name: %[1]s +stringData: + key: "%[2]s" +`, name, data), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id, randStringRunes(5))) + g.Expect(err).NotTo(HaveOccurred(), "failed to create artifact from files") + + repositoryName := types.NamespacedName{ + Name: randStringRunes(5), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision, + withGitRepoArtifactMetadata(OCIArtifactOriginRevisionAnnotation, "orev")) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("force-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + TargetNamespace: id, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + resultK := &kustomizev1.Kustomization{} + readyCondition := &metav1.Condition{} + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + readyCondition = apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) + return resultK.Status.LastAppliedRevision == revision + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(readyCondition.Reason).To(Equal(meta.ReconciliationSucceededReason)) + + g.Expect(resultK.Status.LastAppliedOriginRevision).To(Equal("orev")) + + events := getEvents(kustomizationKey.Name, nil) + g.Expect(events).To(Not(BeEmpty())) + + annotationKey := kustomizev1.GroupVersion.Group + "/" + eventv1.MetaOriginRevisionKey + for _, e := range events { + g.Expect(e.GetAnnotations()).To(HaveKeyWithValue(annotationKey, "orev")) + } +} diff --git a/controllers/kustomization_prune_test.go b/internal/controller/kustomization_prune_test.go similarity index 98% rename from controllers/kustomization_prune_test.go rename to internal/controller/kustomization_prune_test.go index 9207e497e..defada8fc 100644 --- a/controllers/kustomization_prune_test.go +++ b/internal/controller/kustomization_prune_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -22,16 +22,17 @@ import ( "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Prune(t *testing.T) { diff --git a/controllers/kustomization_transformer_test.go b/internal/controller/kustomization_transformer_test.go similarity index 67% rename from controllers/kustomization_transformer_test.go rename to internal/controller/kustomization_transformer_test.go index 7670587ee..1ff2d3b07 100644 --- a/controllers/kustomization_transformer_test.go +++ b/internal/controller/kustomization_transformer_test.go @@ -14,27 +14,223 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" + "fmt" "strings" "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/kustomize" "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/fluxcd/pkg/testserver" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) +func TestKustomizationReconciler_CommonMetadata(t *testing.T) { + g := NewWithT(t) + id := "cm-" + randStringRunes(5) + revision := "v1.0.0" + resultK := &kustomizev1.Kustomization{} + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s + annotations: + tenant: test +data: + key: val +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("cm-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("cm-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: 2 * time.Minute}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + CommonMetadata: &kustomizev1.CommonMetadata{ + Annotations: map[string]string{ + "tenant": id, + }, + Labels: map[string]string{ + "tenant": id, + }, + }, + TargetNamespace: id, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + t.Run("sets labels and annotations", func(t *testing.T) { + g := NewWithT(t) + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return isReconcileSuccess(resultK) + }, timeout, time.Second).Should(BeTrue()) + kstatusCheck.CheckErr(ctx, resultK) + + var cm corev1.ConfigMap + g.Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: id, Namespace: id}, &cm)).To(Succeed()) + g.Expect(cm.GetLabels()).To(HaveKeyWithValue("tenant", id)) + g.Expect(cm.GetAnnotations()).To(HaveKeyWithValue("tenant", id)) + }) + + t.Run("removes labels and annotations", func(t *testing.T) { + g := NewWithT(t) + resultK.Spec.CommonMetadata = nil + g.Expect(k8sClient.Update(context.Background(), resultK)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return isReconcileSuccess(resultK) + }, timeout, time.Second).Should(BeTrue()) + kstatusCheck.CheckErr(ctx, resultK) + + var cm corev1.ConfigMap + g.Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: id, Namespace: id}, &cm)).To(Succeed()) + g.Expect(cm.GetLabels()).ToNot(HaveKeyWithValue("tenant", id)) + g.Expect(cm.GetAnnotations()).ToNot(HaveKeyWithValue("tenant", id)) + }) +} + +func TestKustomizationReconciler_NamePrefixSuffix(t *testing.T) { + g := NewWithT(t) + id := "np-" + randStringRunes(5) + revision := "v1.0.0" + resultK := &kustomizev1.Kustomization{} + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s + annotations: + tenant: test +data: + key: val +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("cm-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("cm-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: 2 * time.Minute}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + NamePrefix: "prefix-", + NameSuffix: "-suffix", + TargetNamespace: id, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + t.Run("sets name prefix and suffix", func(t *testing.T) { + g := NewWithT(t) + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return isReconcileSuccess(resultK) + }, timeout, time.Second).Should(BeTrue()) + kstatusCheck.CheckErr(ctx, resultK) + + name := fmt.Sprintf("prefix-%s-suffix", id) + var cm corev1.ConfigMap + g.Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: name, Namespace: id}, &cm)).To(Succeed()) + }) +} + func TestKustomizationReconciler_KustomizeTransformer(t *testing.T) { g := NewWithT(t) id := "transformers-" + randStringRunes(5) @@ -344,14 +540,14 @@ func TestKustomizationReconciler_FluxTransformers(t *testing.T) { path: /metadata/labels/patch1 value: inline-json `, - Target: kustomize.Selector{ + Target: &kustomize.Selector{ LabelSelector: "app=podinfo", }, }, { Patch: ` -apiVersion: v1 -kind: Pod +apiVersion: apps/v1 +kind: Deployment metadata: name: podinfo labels: @@ -359,25 +555,6 @@ metadata: `, }, }, - PatchesJSON6902: []kustomize.JSON6902Patch{ - { - Patch: []kustomize.JSON6902{ - {Op: "add", Path: "/metadata/labels/patch3", Value: &apiextensionsv1.JSON{Raw: []byte(`"json6902"`)}}, - {Op: "replace", Path: "/spec/replicas", Value: &apiextensionsv1.JSON{Raw: []byte("2")}}, - }, - Target: kustomize.Selector{ - Group: "apps", - Version: "v1", - Kind: "Deployment", - Name: "podinfo", - }, - }, - }, - PatchesStrategicMerge: []apiextensionsv1.JSON{ - { - Raw: []byte(`{"kind":"Deployment","apiVersion":"apps/v1","metadata":{"name":"podinfo","labels":{"patch4":"strategic-merge"}}}`), - }, - }, }, } @@ -395,9 +572,6 @@ metadata: t.Run("applies patches", func(t *testing.T) { g.Expect(deployment.ObjectMeta.Labels["patch1"]).To(Equal("inline-json")) g.Expect(deployment.ObjectMeta.Labels["patch2"]).To(Equal("inline-yaml")) - g.Expect(deployment.ObjectMeta.Labels["patch3"]).To(Equal("json6902")) - g.Expect(deployment.ObjectMeta.Labels["patch4"]).To(Equal("strategic-merge")) - g.Expect(*deployment.Spec.Replicas).To(Equal(int32(2))) g.Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(ContainSubstring("5.2.0")) g.Expect(deployment.Spec.Template.Spec.Containers[1].Image).To(ContainSubstring("sha256:2832f53c577d44753e97b0ed5f00e7e3a06979c9fab77d0e78bdac4b612b14fb")) }) diff --git a/controllers/kustomization_validation_test.go b/internal/controller/kustomization_validation_test.go similarity index 94% rename from controllers/kustomization_validation_test.go rename to internal/controller/kustomization_validation_test.go index 52898f920..4f38ece79 100644 --- a/controllers/kustomization_validation_test.go +++ b/internal/controller/kustomization_validation_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -22,13 +22,14 @@ import ( "testing" "time" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Validation(t *testing.T) { @@ -119,7 +120,7 @@ func TestKustomizationReconciler_Validation(t *testing.T) { g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &resultK) for _, c := range resultK.Status.Conditions { - if c.Reason == kustomizev1.BuildFailedReason { + if c.Reason == meta.BuildFailedReason { return true } } @@ -132,7 +133,7 @@ func TestKustomizationReconciler_Validation(t *testing.T) { g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(overlayKs), &resultK) for _, c := range resultK.Status.Conditions { - if c.Reason == kustomizev1.BuildFailedReason { + if c.Reason == meta.BuildFailedReason { return true } } diff --git a/controllers/kustomization_varsub_test.go b/internal/controller/kustomization_varsub_test.go similarity index 60% rename from controllers/kustomization_varsub_test.go rename to internal/controller/kustomization_varsub_test.go index 2c92cd367..841f46c44 100644 --- a/controllers/kustomization_varsub_test.go +++ b/internal/controller/kustomization_varsub_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -23,7 +23,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -31,7 +31,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_Varsub(t *testing.T) { @@ -164,7 +164,7 @@ stringData: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(inputK), resultK) for _, c := range resultK.Status.Conditions { - if c.Reason == kustomizev1.ReconciliationSucceededReason { + if c.Reason == meta.ReconciliationSucceededReason { return true } } @@ -178,7 +178,7 @@ stringData: t.Run("sets status", func(t *testing.T) { g.Expect(resultK.Status.LastAppliedRevision).To(Equal(revision)) g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.ReadyCondition)).To(BeTrue()) - g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, kustomizev1.HealthyCondition)).To(BeTrue()) + g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.HealthyCondition)).To(BeTrue()) }) t.Run("replaces vars", func(t *testing.T) { @@ -315,7 +315,7 @@ metadata: resultK := &kustomizev1.Kustomization{} _ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK) for _, c := range resultK.Status.Conditions { - if c.Reason == kustomizev1.ReconciliationSucceededReason { + if c.Reason == meta.ReconciliationSucceededReason { return true } } @@ -349,3 +349,224 @@ metadata: g.Expect(resultSA.Labels["shape"]).To(Equal("square")) }) } + +func TestKustomizationReconciler_VarsubNumberBool(t *testing.T) { + ctx := context.Background() + + g := NewWithT(t) + id := "vars-" + randStringRunes(5) + revision := "v1.0.0/" + randStringRunes(7) + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string) []testserver.File { + return []testserver.File{ + { + Name: "templates.yaml", + Body: fmt.Sprintf(` +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: %[1]s + namespace: %[1]s + labels: + id: ${numberStr} + enabled: ${booleanStr} + annotations: + id: ${q}${number}${q} + enabled: ${q}${boolean}${q} +spec: + interval: ${number}m + url: https://host/repo +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s + namespace: %[1]s +data: + id: ${q}${number}${q} + text: | + This variable is escaped $${var} + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at + nisl sem. Nullam nec dui ipsum. Nam vehicula volutpat ipsum, ac fringilla + nisl convallis sed. Aliquam porttitor turpis finibus, finibus velit ut, + imperdiet mauris. Cras nec neque nulla. Maecenas semper nulla et elit + dictum sagittis. Quisque tincidunt non diam non ullamcorper. Curabitur + pretium urna odio, vitae ullamcorper purus mollis sit amet. Nam ac lectus + ac arcu varius feugiat id fringilla massa. + + \? +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: randStringRunes(5), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + inputK := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: id, + Namespace: id, + }, + Spec: kustomizev1.KustomizationSpec{ + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + Prune: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repositoryName.Name, + }, + PostBuild: &kustomizev1.PostBuild{ + Substitute: map[string]string{ + "q": `"`, + + "numberStr": "!!str 123", + "number": "123", + "booleanStr": "!!str true", + "boolean": "true", + }, + }, + Wait: false, + }, + } + g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed()) + + g.Eventually(func() bool { + resultK := &kustomizev1.Kustomization{} + _ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK) + for _, c := range resultK.Status.Conditions { + if c.Reason == meta.ReconciliationSucceededReason { + return true + } + } + return false + }, timeout, interval).Should(BeTrue()) + + resultRepo := &sourcev1.GitRepository{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultRepo)).Should(Succeed()) + g.Expect(resultRepo.Labels["id"]).To(Equal("123")) + g.Expect(resultRepo.Annotations["id"]).To(Equal("123")) + g.Expect(resultRepo.Labels["enabled"]).To(Equal("true")) + g.Expect(resultRepo.Annotations["enabled"]).To(Equal("true")) + + resultCM := &corev1.ConfigMap{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultCM)).Should(Succeed()) + g.Expect(resultCM.Data["id"]).To(Equal("123")) + g.Expect(resultCM.Data["text"]).To(ContainSubstring(`${var}`)) + g.Expect(resultCM.Data["text"]).ToNot(ContainSubstring(`$${var}`)) + g.Expect(resultCM.Data["text"]).To(ContainSubstring(`\?`)) +} + +func TestKustomizationReconciler_VarsubStrict(t *testing.T) { + reconciler.StrictSubstitutions = true + defer func() { + reconciler.StrictSubstitutions = false + }() + + ctx := context.Background() + + g := NewWithT(t) + id := "vars-" + randStringRunes(5) + revision := "v1.0.0/" + randStringRunes(7) + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string) []testserver.File { + return []testserver.File{ + { + Name: "service-account.yaml", + Body: fmt.Sprintf(` +apiVersion: v1 +kind: ServiceAccount +metadata: + name: %[1]s + namespace: %[1]s + labels: + default: ${default:=test} + missing: ${missing} +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: randStringRunes(5), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + inputK := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: id, + Namespace: id, + }, + Spec: kustomizev1.KustomizationSpec{ + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + Prune: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: repositoryName.Name, + }, + PostBuild: &kustomizev1.PostBuild{ + Substitute: map[string]string{ + "test": "test", + }, + }, + Wait: true, + }, + } + g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed()) + + var resultK kustomizev1.Kustomization + t.Run("fails to reconcile", func(t *testing.T) { + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(inputK), &resultK) + for _, c := range resultK.Status.Conditions { + if c.Reason == meta.BuildFailedReason { + return true + } + } + return false + }, timeout, interval).Should(BeTrue()) + }) + + ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) + g.Expect(ready.Message).To(ContainSubstring("variable not set")) + g.Expect(k8sClient.Delete(context.Background(), &resultK)).To(Succeed()) +} diff --git a/controllers/kustomization_wait_test.go b/internal/controller/kustomization_wait_test.go similarity index 57% rename from controllers/kustomization_wait_test.go rename to internal/controller/kustomization_wait_test.go index 3f0da7077..a4c663115 100644 --- a/controllers/kustomization_wait_test.go +++ b/internal/controller/kustomization_wait_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" @@ -22,19 +22,22 @@ import ( "testing" "time" + runtimeClient "github.com/fluxcd/pkg/runtime/client" . "github.com/onsi/gomega" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/fluxcd/pkg/apis/kustomize" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestKustomizationReconciler_WaitConditions(t *testing.T) { @@ -43,6 +46,7 @@ func TestKustomizationReconciler_WaitConditions(t *testing.T) { revision := "v1.0.0" resultK := &kustomizev1.Kustomization{} reconcileRequestAt := metav1.Now().String() + timeout := 60 * time.Second err := createNamespace(id) g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") @@ -126,8 +130,8 @@ parameters: }, timeout, time.Second).Should(BeTrue()) logStatus(t, resultK) - g.Expect(conditions.IsTrue(resultK, kustomizev1.HealthyCondition)).To(BeTrue()) - g.Expect(conditions.GetReason(resultK, kustomizev1.HealthyCondition)).To(BeIdenticalTo(meta.SucceededReason)) + g.Expect(conditions.IsTrue(resultK, meta.HealthyCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(resultK, meta.HealthyCondition)).To(BeIdenticalTo(meta.SucceededReason)) g.Expect(resultK.Status.ObservedGeneration).To(BeIdenticalTo(resultK.Generation)) @@ -154,12 +158,12 @@ parameters: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) - return isReconcileRunning(resultK) && conditions.IsUnknown(resultK, kustomizev1.HealthyCondition) + return isReconcileRunning(resultK) && conditions.IsUnknown(resultK, meta.HealthyCondition) }, timeout, time.Second).Should(BeTrue()) logStatus(t, resultK) expectedMessage := "Running health checks" - for _, c := range []string{meta.ReconcilingCondition, kustomizev1.HealthyCondition} { + for _, c := range []string{meta.ReconcilingCondition, meta.HealthyCondition} { g.Expect(conditions.GetReason(resultK, c)).To(BeIdenticalTo(meta.ProgressingReason)) g.Expect(conditions.GetMessage(resultK, c)).To(ContainSubstring(expectedMessage)) g.Expect(conditions.GetObservedGeneration(resultK, c)).To(BeIdenticalTo(resultK.Generation)) @@ -174,14 +178,14 @@ parameters: }, timeout, time.Second).Should(BeTrue()) logStatus(t, resultK) - for _, c := range []string{kustomizev1.HealthyCondition, meta.ReadyCondition} { + for _, c := range []string{meta.HealthyCondition, meta.ReadyCondition} { g.Expect(conditions.IsFalse(resultK, c)).To(BeTrue()) - g.Expect(conditions.GetReason(resultK, c)).To(BeIdenticalTo(kustomizev1.HealthCheckFailedReason)) + g.Expect(conditions.GetReason(resultK, c)).To(BeIdenticalTo(meta.HealthCheckFailedReason)) g.Expect(conditions.GetObservedGeneration(resultK, c)).To(BeIdenticalTo(resultK.Generation)) } expectedMessage := "Running health checks" - g.Expect(conditions.GetReason(resultK, meta.ReconcilingCondition)).To(BeIdenticalTo(kustomizev1.ProgressingWithRetryReason)) + g.Expect(conditions.GetReason(resultK, meta.ReconcilingCondition)).To(BeIdenticalTo(meta.ProgressingWithRetryReason)) g.Expect(conditions.GetMessage(resultK, meta.ReconcilingCondition)).To(ContainSubstring(expectedMessage)) g.Expect(resultK.Status.LastHandledReconcileAt).To(BeIdenticalTo(reconcileRequestAt)) @@ -211,13 +215,13 @@ parameters: logStatus(t, resultK) expectedMessage := "Health check passed" - g.Expect(conditions.IsTrue(resultK, kustomizev1.HealthyCondition)).To(BeTrue()) - g.Expect(conditions.GetReason(resultK, kustomizev1.HealthyCondition)).To(BeIdenticalTo(meta.SucceededReason)) - g.Expect(conditions.GetObservedGeneration(resultK, kustomizev1.HealthyCondition)).To(BeIdenticalTo(resultK.Generation)) - g.Expect(conditions.GetMessage(resultK, kustomizev1.HealthyCondition)).To(ContainSubstring(expectedMessage)) + g.Expect(conditions.IsTrue(resultK, meta.HealthyCondition)).To(BeTrue()) + g.Expect(conditions.GetReason(resultK, meta.HealthyCondition)).To(BeIdenticalTo(meta.SucceededReason)) + g.Expect(conditions.GetObservedGeneration(resultK, meta.HealthyCondition)).To(BeIdenticalTo(resultK.Generation)) + g.Expect(conditions.GetMessage(resultK, meta.HealthyCondition)).To(ContainSubstring(expectedMessage)) g.Expect(conditions.IsTrue(resultK, meta.ReadyCondition)).To(BeTrue()) - g.Expect(conditions.GetReason(resultK, meta.ReadyCondition)).To(BeIdenticalTo(kustomizev1.ReconciliationSucceededReason)) + g.Expect(conditions.GetReason(resultK, meta.ReadyCondition)).To(BeIdenticalTo(meta.ReconciliationSucceededReason)) g.Expect(conditions.GetObservedGeneration(resultK, meta.ReadyCondition)).To(BeIdenticalTo(resultK.Generation)) g.Expect(conditions.GetMessage(resultK, meta.ReadyCondition)).To(BeIdenticalTo(fmt.Sprintf("Applied revision: %s", revision))) @@ -248,7 +252,7 @@ parameters: logStatus(t, resultK) g.Expect(isReconcileSuccess(resultK)).To(BeTrue()) - g.Expect(conditions.IsTrue(resultK, kustomizev1.HealthyCondition)).To(BeTrue()) + g.Expect(conditions.IsTrue(resultK, meta.HealthyCondition)).To(BeTrue()) g.Expect(conditions.GetMessage(resultK, meta.ReadyCondition)).To(BeIdenticalTo(fmt.Sprintf("Applied revision: %s", revision))) g.Expect(resultK.Status.LastAttemptedRevision).To(BeIdenticalTo(resultK.Status.LastAppliedRevision)) @@ -274,3 +278,171 @@ parameters: }, timeout, time.Second).Should(BeTrue()) }) } + +func TestKustomizationReconciler_WaitsForCustomHealthChecks(t *testing.T) { + g := NewWithT(t) + id := "cel-" + randStringRunes(5) + revision := "v1.0.0" + resultK := &kustomizev1.Kustomization{} + timeout := 60 * time.Second + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: {} +`, name), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("wait-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("wait-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: 2 * time.Minute}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + TargetNamespace: id, + Prune: true, + Timeout: &metav1.Duration{Duration: time.Second}, + Wait: true, + HealthCheckExprs: []kustomize.CustomHealthCheck{{ + APIVersion: "v1", + Kind: "ConfigMap", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + InProgress: "has(data.foo.bar)", + Current: "true", + }, + }}, + }, + } + + err = k8sClient.Create(context.Background(), kustomization) + g.Expect(err).NotTo(HaveOccurred()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.IsFalse(resultK, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + logStatus(t, resultK) + + msg := conditions.GetMessage(resultK, meta.ReadyCondition) + g.Expect(msg). + To(ContainSubstring("timeout waiting for: [ConfigMap")) + g.Expect(msg). + To(ContainSubstring("failed to evaluate the CEL expression 'has(data.foo.bar)': no such attribute(s): data.foo.bar")) +} + +func TestKustomizationReconciler_RESTMapper(t *testing.T) { + g := NewWithT(t) + id := "rm-" + randStringRunes(5) + resultK := &kustomizev1.Kustomization{} + + restMapper, err := runtimeClient.NewDynamicRESTMapper(testEnv.Config) + g.Expect(err).NotTo(HaveOccurred()) + + err = createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + artifactName := "val-" + randStringRunes(5) + artifactChecksum, err := testServer.ArtifactFromDir("testdata/restmapper", artifactName) + g.Expect(err).ToNot(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("val-%s", randStringRunes(5)), + Namespace: id, + } + + err = applyGitRepository(repositoryName, artifactName, "main/"+artifactChecksum) + g.Expect(err).NotTo(HaveOccurred()) + + kustomization := &kustomizev1.Kustomization{} + kustomization.Name = id + kustomization.Namespace = id + kustomization.Spec = kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: 10 * time.Minute}, + Prune: true, + Path: "./", + Wait: true, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + KubeConfig: &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return isReconcileSuccess(resultK) && resultK.Status.LastAttemptedRevision == "main/"+artifactChecksum + }, timeout, time.Second).Should(BeTrue()) + + t.Run("discovers newly registered CRD and preferred version", func(t *testing.T) { + mapping, err := restMapper.RESTMapping(schema.GroupKind{Kind: "ClusterCleanupPolicy", Group: "kyverno.io"}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(mapping.Resource.Version).To(Equal("v2")) + }) + + t.Run("finalizes object", func(t *testing.T) { + g.Expect(k8sClient.Delete(context.Background(), resultK)).To(Succeed()) + + g.Eventually(func() bool { + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return apierrors.IsNotFound(err) + }, timeout, time.Second).Should(BeTrue()) + }) + + t.Run("discovery fails for deleted CRD", func(t *testing.T) { + newMapper, err := runtimeClient.NewDynamicRESTMapper(testEnv.Config) + g.Expect(err).NotTo(HaveOccurred()) + _, err = newMapper.RESTMapping(schema.GroupKind{Kind: "ClusterCleanupPolicy", Group: "kyverno.io"}) + g.Expect(err).To(HaveOccurred()) + }) +} diff --git a/controllers/source_predicate.go b/internal/controller/source_predicate.go similarity index 88% rename from controllers/source_predicate.go rename to internal/controller/source_predicate.go index 1909f05fd..0a2b53478 100644 --- a/controllers/source_predicate.go +++ b/internal/controller/source_predicate.go @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) type SourceRevisionChangePredicate struct { @@ -47,7 +47,7 @@ func (SourceRevisionChangePredicate) Update(e event.UpdateEvent) bool { } if oldSource.GetArtifact() != nil && newSource.GetArtifact() != nil && - oldSource.GetArtifact().Revision != newSource.GetArtifact().Revision { + !oldSource.GetArtifact().HasRevision(newSource.GetArtifact().Revision) { return true } diff --git a/controllers/suite_test.go b/internal/controller/suite_test.go similarity index 80% rename from controllers/suite_test.go rename to internal/controller/suite_test.go index 5b976933c..9ef8536ea 100644 --- a/controllers/suite_test.go +++ b/internal/controller/suite_test.go @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "context" - "crypto/sha256" "fmt" "math/rand" "os" @@ -27,6 +26,7 @@ import ( "time" "github.com/hashicorp/vault/api" + "github.com/opencontainers/go-digest" "github.com/ory/dockertest/v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,25 +43,22 @@ import ( "github.com/fluxcd/pkg/runtime/conditions" kcheck "github.com/fluxcd/pkg/runtime/conditions/check" "github.com/fluxcd/pkg/runtime/controller" + "github.com/fluxcd/pkg/runtime/metrics" "github.com/fluxcd/pkg/runtime/testenv" "github.com/fluxcd/pkg/testserver" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - const ( timeout = time.Second * 30 interval = time.Second * 1 reconciliationInterval = time.Second * 5 + vaultVersion = "1.13.2" + overrideManagerName = "node-fetch" ) -const vaultVersion = "1.2.2" - var ( reconciler *KustomizationReconciler k8sClient client.Client @@ -75,16 +72,19 @@ var ( debugMode = os.Getenv("DEBUG_TEST") != "" ) -func runInContext(registerControllers func(*testenv.Environment), run func() error, crdPath string) error { +func runInContext(registerControllers func(*testenv.Environment), run func() int) (code int) { var err error - utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) utilruntime.Must(kustomizev1.AddToScheme(scheme.Scheme)) + utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) if debugMode { controllerLog.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(false))) } - testEnv = testenv.New(testenv.WithCRDPath(crdPath)) + testEnv = testenv.New( + testenv.WithCRDPath(filepath.Join("..", "..", "config", "crd", "bases")), + testenv.WithMaxConcurrentReconciles(4), + ) testServer, err = testserver.NewTempArtifactServer() if err != nil { @@ -131,7 +131,7 @@ func runInContext(registerControllers func(*testenv.Environment), run func() err pool.Purge(resource) }() - runErr := run() + code = run() if debugMode { events := &corev1.EventList{} @@ -154,15 +154,13 @@ func runInContext(registerControllers func(*testenv.Environment), run func() err panic(fmt.Sprintf("Failed to remove storage server dir: %v", err)) } - return runErr + return code } func TestMain(m *testing.M) { - code := 0 - - runInContext(func(testEnv *testenv.Environment) { + code := runInContext(func(testEnv *testenv.Environment) { controllerName := "kustomize-controller" - testMetricsH = controller.MustMakeMetrics(testEnv) + testMetricsH = controller.NewMetrics(testEnv, metrics.MustMakeRecorder(), kustomizev1.KustomizationFinalizer) kstatusCheck = kcheck.NewChecker(testEnv.Client, &kcheck.Conditions{ NegativePolarity: []string{meta.StalledCondition, meta.ReconcilingCondition}, @@ -174,21 +172,21 @@ func TestMain(m *testing.M) { kstatusInProgressCheck = kcheck.NewInProgressChecker(testEnv.Client) kstatusInProgressCheck.DisableFetch = true reconciler = &KustomizationReconciler{ - ControllerName: controllerName, - Client: testEnv, - EventRecorder: testEnv.GetEventRecorderFor(controllerName), - Metrics: testMetricsH, + ControllerName: controllerName, + Client: testEnv, + Mapper: testEnv.GetRESTMapper(), + APIReader: testEnv, + EventRecorder: testEnv.GetEventRecorderFor(controllerName), + Metrics: testMetricsH, + ConcurrentSSA: 4, + DisallowedFieldManagers: []string{overrideManagerName}, } - if err := (reconciler).SetupWithManager(testEnv, KustomizationReconcilerOptions{ - MaxConcurrentReconciles: 4, + if err := (reconciler).SetupWithManager(ctx, testEnv, KustomizationReconcilerOptions{ DependencyRequeueInterval: 2 * time.Second, }); err != nil { panic(fmt.Sprintf("Failed to start KustomizationReconciler: %v", err)) } - }, func() error { - code = m.Run() - return nil - }, filepath.Join("..", "config", "crd", "bases")) + }, m.Run) os.Exit(code) } @@ -205,7 +203,7 @@ func randStringRunes(n int) string { func isReconcileRunning(k *kustomizev1.Kustomization) bool { return conditions.IsReconciling(k) && - conditions.GetReason(k, meta.ReconcilingCondition) != kustomizev1.ProgressingWithRetryReason + conditions.GetReason(k, meta.ReconcilingCondition) != meta.ProgressingWithRetryReason } func isReconcileSuccess(k *kustomizev1.Kustomization) bool { @@ -228,7 +226,7 @@ func isReconcileFailure(k *kustomizev1.Kustomization) bool { return isHandled && conditions.IsReconciling(k) && conditions.IsFalse(k, meta.ReadyCondition) && conditions.GetObservedGeneration(k, meta.ReadyCondition) == k.Generation && - conditions.GetReason(k, meta.ReconcilingCondition) == kustomizev1.ProgressingWithRetryReason + conditions.GetReason(k, meta.ReconcilingCondition) == meta.ProgressingWithRetryReason } func logStatus(t *testing.T, k *kustomizev1.Kustomization) { @@ -277,7 +275,29 @@ func createKubeConfigSecret(namespace string) error { return k8sClient.Create(context.Background(), secret) } -func applyGitRepository(objKey client.ObjectKey, artifactName string, revision string) error { +type gitRepoOption func(*gitRepoOptions) + +type gitRepoOptions struct { + artifactMetadata map[string]string +} + +func withGitRepoArtifactMetadata(k, v string) gitRepoOption { + return func(o *gitRepoOptions) { + if o.artifactMetadata == nil { + o.artifactMetadata = make(map[string]string) + } + o.artifactMetadata[k] = v + } +} + +func applyGitRepository(objKey client.ObjectKey, artifactName string, + revision string, opts ...gitRepoOption) error { + + var opt gitRepoOptions + for _, o := range opts { + o(&opt) + } + repo := &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, @@ -294,7 +314,7 @@ func applyGitRepository(objKey client.ObjectKey, artifactName string, revision s } b, _ := os.ReadFile(filepath.Join(testServer.Root(), artifactName)) - checksum := fmt.Sprintf("%x", sha256.Sum256(b)) + dig := digest.SHA256.FromBytes(b) url := fmt.Sprintf("%s/%s", testServer.URL(), artifactName) @@ -311,17 +331,18 @@ func applyGitRepository(objKey client.ObjectKey, artifactName string, revision s Path: url, URL: url, Revision: revision, - Checksum: checksum, + Digest: dig.String(), LastUpdateTime: metav1.Now(), + Metadata: opt.artifactMetadata, }, } - opt := []client.PatchOption{ + patchOpts := []client.PatchOption{ client.ForceOwnership, client.FieldOwner("kustomize-controller"), } - if err := k8sClient.Patch(context.Background(), repo, client.Apply, opt...); err != nil { + if err := k8sClient.Patch(context.Background(), repo, client.Apply, patchOpts...); err != nil { return err } @@ -344,13 +365,13 @@ func createVaultTestInstance() (*dockertest.Pool, *dockertest.Resource, error) { // uses a sensible default on windows (tcp/http) and linux/osx (socket) pool, err := dockertest.NewPool("") if err != nil { - return nil, nil, fmt.Errorf("Could not connect to docker: %s", err) + return nil, nil, fmt.Errorf("could not connect to docker: %s", err) } // pulls an image, creates a container based on it and runs it resource, err := pool.Run("vault", vaultVersion, []string{"VAULT_DEV_ROOT_TOKEN_ID=secret"}) if err != nil { - return nil, nil, fmt.Errorf("Could not start resource: %s", err) + return nil, nil, fmt.Errorf("could not start resource: %s", err) } os.Setenv("VAULT_ADDR", fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8200/tcp"))) @@ -359,24 +380,24 @@ func createVaultTestInstance() (*dockertest.Pool, *dockertest.Resource, error) { if err := pool.Retry(func() error { cli, err := api.NewClient(api.DefaultConfig()) if err != nil { - return fmt.Errorf("Cannot create Vault Client: %w", err) + return fmt.Errorf("cannot create Vault Client: %w", err) } status, err := cli.Sys().InitStatus() if err != nil { return err } if status != true { - return fmt.Errorf("Vault not ready yet") + return fmt.Errorf("vault not ready yet") } if err := cli.Sys().Mount("sops", &api.MountInput{ Type: "transit", }); err != nil { - return fmt.Errorf("Cannot create Vault Transit Engine: %w", err) + return fmt.Errorf("cannot create Vault Transit Engine: %w", err) } return nil }); err != nil { - return nil, nil, fmt.Errorf("Could not connect to docker: %w", err) + return nil, nil, fmt.Errorf("could not connect to docker: %w", err) } return pool, resource, nil diff --git a/internal/controller/testdata/crds/crd.yaml b/internal/controller/testdata/crds/crd.yaml new file mode 100644 index 000000000..0c218b64e --- /dev/null +++ b/internal/controller/testdata/crds/crd.yaml @@ -0,0 +1,74 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: namespaces.servicebus.azure.com +spec: + group: servicebus.azure.com + names: + kind: Namespace + listKind: NamespaceList + plural: namespaces + singular: namespace + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.type + name: TYPE + type: string + name: v1beta20210101preview + schema: + openAPIV3Schema: + description: Test is the Schema for the testing API + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: TestSpec defines the desired state of a test run + properties: + type: + description: Type of test + type: string + enum: + - unit + - integration + valuesFrom: + description: config reference + type: string + type: object + status: + default: + observedGeneration: -1 + properties: + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: servicebus.azure.com/v1beta20210101preview +kind: Namespace +metadata: + annotations: + serviceoperator.azure.com/reconcile-policy: detach-on-delete + name: sptribs-servicebus-preview + namespace: sptribs +spec: + type: integration + valuesFrom: test-config +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + slackChannel: special-tribunals-builds + name: sptribs + diff --git a/controllers/testdata/file-transformer/annotation-transformer.yaml b/internal/controller/testdata/file-transformer/annotation-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/annotation-transformer.yaml rename to internal/controller/testdata/file-transformer/annotation-transformer.yaml diff --git a/controllers/testdata/file-transformer/configmap-generator.yaml b/internal/controller/testdata/file-transformer/configmap-generator.yaml similarity index 100% rename from controllers/testdata/file-transformer/configmap-generator.yaml rename to internal/controller/testdata/file-transformer/configmap-generator.yaml diff --git a/controllers/testdata/file-transformer/deployment.yaml b/internal/controller/testdata/file-transformer/deployment.yaml similarity index 100% rename from controllers/testdata/file-transformer/deployment.yaml rename to internal/controller/testdata/file-transformer/deployment.yaml diff --git a/controllers/testdata/file-transformer/imagetag-transformer.yaml b/internal/controller/testdata/file-transformer/imagetag-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/imagetag-transformer.yaml rename to internal/controller/testdata/file-transformer/imagetag-transformer.yaml diff --git a/controllers/testdata/file-transformer/kustomization.yaml b/internal/controller/testdata/file-transformer/kustomization.yaml similarity index 100% rename from controllers/testdata/file-transformer/kustomization.yaml rename to internal/controller/testdata/file-transformer/kustomization.yaml diff --git a/controllers/testdata/file-transformer/label-transformer.yaml b/internal/controller/testdata/file-transformer/label-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/label-transformer.yaml rename to internal/controller/testdata/file-transformer/label-transformer.yaml diff --git a/controllers/testdata/file-transformer/namespace-transformer.yaml b/internal/controller/testdata/file-transformer/namespace-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/namespace-transformer.yaml rename to internal/controller/testdata/file-transformer/namespace-transformer.yaml diff --git a/controllers/testdata/file-transformer/patch-transformer.yaml b/internal/controller/testdata/file-transformer/patch-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/patch-transformer.yaml rename to internal/controller/testdata/file-transformer/patch-transformer.yaml diff --git a/controllers/testdata/file-transformer/patch.yaml b/internal/controller/testdata/file-transformer/patch.yaml similarity index 100% rename from controllers/testdata/file-transformer/patch.yaml rename to internal/controller/testdata/file-transformer/patch.yaml diff --git a/controllers/testdata/file-transformer/patchStrategicMerge-transformer.yaml b/internal/controller/testdata/file-transformer/patchStrategicMerge-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/patchStrategicMerge-transformer.yaml rename to internal/controller/testdata/file-transformer/patchStrategicMerge-transformer.yaml diff --git a/controllers/testdata/file-transformer/patchjson6902-transformer.yaml b/internal/controller/testdata/file-transformer/patchjson6902-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/patchjson6902-transformer.yaml rename to internal/controller/testdata/file-transformer/patchjson6902-transformer.yaml diff --git a/controllers/testdata/file-transformer/prefixsuffix-transformer.yaml b/internal/controller/testdata/file-transformer/prefixsuffix-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/prefixsuffix-transformer.yaml rename to internal/controller/testdata/file-transformer/prefixsuffix-transformer.yaml diff --git a/controllers/testdata/file-transformer/quota.yaml b/internal/controller/testdata/file-transformer/quota.yaml similarity index 100% rename from controllers/testdata/file-transformer/quota.yaml rename to internal/controller/testdata/file-transformer/quota.yaml diff --git a/controllers/testdata/file-transformer/replacement-transformer.yaml b/internal/controller/testdata/file-transformer/replacement-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/replacement-transformer.yaml rename to internal/controller/testdata/file-transformer/replacement-transformer.yaml diff --git a/controllers/testdata/file-transformer/replicas-transformer.yaml b/internal/controller/testdata/file-transformer/replicas-transformer.yaml similarity index 100% rename from controllers/testdata/file-transformer/replicas-transformer.yaml rename to internal/controller/testdata/file-transformer/replicas-transformer.yaml diff --git a/controllers/testdata/file-transformer/secret-generator.yaml b/internal/controller/testdata/file-transformer/secret-generator.yaml similarity index 100% rename from controllers/testdata/file-transformer/secret-generator.yaml rename to internal/controller/testdata/file-transformer/secret-generator.yaml diff --git a/controllers/testdata/invalid/overlay/deployment.yaml b/internal/controller/testdata/invalid/overlay/deployment.yaml similarity index 100% rename from controllers/testdata/invalid/overlay/deployment.yaml rename to internal/controller/testdata/invalid/overlay/deployment.yaml diff --git a/controllers/testdata/invalid/overlay/kustomization.yaml b/internal/controller/testdata/invalid/overlay/kustomization.yaml similarity index 100% rename from controllers/testdata/invalid/overlay/kustomization.yaml rename to internal/controller/testdata/invalid/overlay/kustomization.yaml diff --git a/controllers/testdata/invalid/plain/deployment.yaml b/internal/controller/testdata/invalid/plain/deployment.yaml similarity index 100% rename from controllers/testdata/invalid/plain/deployment.yaml rename to internal/controller/testdata/invalid/plain/deployment.yaml diff --git a/controllers/testdata/patch/deployment.yaml b/internal/controller/testdata/patch/deployment.yaml similarity index 100% rename from controllers/testdata/patch/deployment.yaml rename to internal/controller/testdata/patch/deployment.yaml diff --git a/internal/controller/testdata/restmapper/cr.yaml b/internal/controller/testdata/restmapper/cr.yaml new file mode 100644 index 000000000..9e55fcd30 --- /dev/null +++ b/internal/controller/testdata/restmapper/cr.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v2 +kind: ClusterCleanupPolicy +metadata: + name: test-cluster-cleanup-policy +spec: + conditions: + all: + - key: '{{ time_since('''', ''{{ target.metadata.creationTimestamp }}'', '''') }}' + operator: GreaterThan + value: 168h + match: + any: + - resources: + annotations: + openshift.io/description: review-* + openshift.io/requester: system:serviceaccount:* + kinds: + - Namespace + selector: + matchLabels: + test/project-name: "review" + schedule: '*/5 * * * *' diff --git a/internal/controller/testdata/restmapper/crd.yaml b/internal/controller/testdata/restmapper/crd.yaml new file mode 100644 index 000000000..e78163c0f --- /dev/null +++ b/internal/controller/testdata/restmapper/crd.yaml @@ -0,0 +1,2590 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: clustercleanuppolicies.kyverno.io +spec: + conversion: + strategy: None + group: kyverno.io + names: + categories: + - kyverno + kind: ClusterCleanupPolicy + listKind: ClusterCleanupPolicyList + plural: clustercleanuppolicies + shortNames: + - ccleanpol + singular: clustercleanuppolicy + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.schedule + name: Schedule + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v2 + schema: + openAPIV3Schema: + description: ClusterCleanupPolicy defines rule for resource cleanup. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec declares policy behaviors. + properties: + conditions: + description: Conditions defines the conditions used to select the + resources which will be cleaned up. + properties: + all: + description: |- + AllConditions enable variable-based conditional rule execution. This is useful for + finer control of when an rule is applied. A condition can reference object data + using JMESPath notation. + Here, all of the conditions need to pass. + items: + properties: + key: + description: Key is the context entry (using JMESPath) for + conditional rule evaluation. + x-kubernetes-preserve-unknown-fields: true + message: + description: Message is an optional display message + type: string + operator: + description: |- + Operator is the conditional operation to perform. Valid operators are: + Equals, NotEquals, In, AnyIn, AllIn, NotIn, AnyNotIn, AllNotIn, GreaterThanOrEquals, + GreaterThan, LessThanOrEquals, LessThan, DurationGreaterThanOrEquals, DurationGreaterThan, + DurationLessThanOrEquals, DurationLessThan + enum: + - Equals + - NotEquals + - AnyIn + - AllIn + - AnyNotIn + - AllNotIn + - GreaterThanOrEquals + - GreaterThan + - LessThanOrEquals + - LessThan + - DurationGreaterThanOrEquals + - DurationGreaterThan + - DurationLessThanOrEquals + - DurationLessThan + type: string + value: + description: |- + Value is the conditional value, or set of values. The values can be fixed set + or can be variables declared using JMESPath. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + any: + description: |- + AnyConditions enable variable-based conditional rule execution. This is useful for + finer control of when an rule is applied. A condition can reference object data + using JMESPath notation. + Here, at least one of the conditions need to pass. + items: + properties: + key: + description: Key is the context entry (using JMESPath) for + conditional rule evaluation. + x-kubernetes-preserve-unknown-fields: true + message: + description: Message is an optional display message + type: string + operator: + description: |- + Operator is the conditional operation to perform. Valid operators are: + Equals, NotEquals, In, AnyIn, AllIn, NotIn, AnyNotIn, AllNotIn, GreaterThanOrEquals, + GreaterThan, LessThanOrEquals, LessThan, DurationGreaterThanOrEquals, DurationGreaterThan, + DurationLessThanOrEquals, DurationLessThan + enum: + - Equals + - NotEquals + - AnyIn + - AllIn + - AnyNotIn + - AllNotIn + - GreaterThanOrEquals + - GreaterThan + - LessThanOrEquals + - LessThan + - DurationGreaterThanOrEquals + - DurationGreaterThan + - DurationLessThanOrEquals + - DurationLessThan + type: string + value: + description: |- + Value is the conditional value, or set of values. The values can be fixed set + or can be variables declared using JMESPath. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + type: object + context: + description: Context defines variables and data sources that can be + used during rule execution. + items: + description: |- + ContextEntry adds variables and data sources to a rule Context. Either a + ConfigMap reference or a APILookup must be provided. + oneOf: + - required: + - configMap + - required: + - apiCall + - required: + - imageRegistry + - required: + - variable + - required: + - globalReference + properties: + apiCall: + description: |- + APICall is an HTTP request to the Kubernetes API server, or other JSON web service. + The data returned is stored in the context with the name for the context entry. + properties: + data: + description: |- + The data object specifies the POST data sent to the server. + Only applicable when the method field is set to POST. + items: + description: RequestData contains the HTTP POST data + properties: + key: + description: Key is a unique identifier for the data + value + type: string + value: + description: Value is the data value + x-kubernetes-preserve-unknown-fields: true + required: + - key + - value + type: object + type: array + default: + description: |- + Default is an optional arbitrary JSON object that the context + value is set to, if the apiCall returns error. + x-kubernetes-preserve-unknown-fields: true + jmesPath: + description: |- + JMESPath is an optional JSON Match Expression that can be used to + transform the JSON response returned from the server. For example + a JMESPath of "items | length(@)" applied to the API server response + for the URLPath "/apis/apps/v1/deployments" will return the total count + of deployments across all namespaces. + type: string + method: + default: GET + description: Method is the HTTP request type (GET or POST). + Defaults to GET. + enum: + - GET + - POST + type: string + service: + description: |- + Service is an API call to a JSON web service. + This is used for non-Kubernetes API server calls. + It's mutually exclusive with the URLPath field. + properties: + caBundle: + description: |- + CABundle is a PEM encoded CA bundle which will be used to validate + the server certificate. + type: string + headers: + description: Headers is a list of optional HTTP headers + to be included in the request. + items: + properties: + key: + description: Key is the header key + type: string + value: + description: Value is the header value + type: string + required: + - key + - value + type: object + type: array + url: + description: |- + URL is the JSON web service URL. A typical form is + `https://{service}.{namespace}:{port}/{path}`. + type: string + required: + - url + type: object + urlPath: + description: |- + URLPath is the URL path to be used in the HTTP GET or POST request to the + Kubernetes API server (e.g. "/api/v1/namespaces" or "/apis/apps/v1/deployments"). + The format required is the same format used by the `kubectl get --raw` command. + See https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls + for details. + It's mutually exclusive with the Service field. + type: string + type: object + configMap: + description: ConfigMap is the ConfigMap reference. + properties: + name: + description: Name is the ConfigMap name. + type: string + namespace: + description: Namespace is the ConfigMap namespace. + type: string + required: + - name + type: object + globalReference: + description: GlobalContextEntryReference is a reference to a + cached global context entry. + properties: + jmesPath: + description: |- + JMESPath is an optional JSON Match Expression that can be used to + transform the JSON response returned from the server. For example + a JMESPath of "items | length(@)" applied to the API server response + for the URLPath "/apis/apps/v1/deployments" will return the total count + of deployments across all namespaces. + type: string + name: + description: Name of the global context entry + type: string + required: + - name + type: object + imageRegistry: + description: |- + ImageRegistry defines requests to an OCI/Docker V2 registry to fetch image + details. + properties: + imageRegistryCredentials: + description: ImageRegistryCredentials provides credentials + that will be used for authentication with registry + properties: + allowInsecureRegistry: + description: AllowInsecureRegistry allows insecure access + to a registry. + type: boolean + providers: + description: |- + Providers specifies a list of OCI Registry names, whose authentication providers are provided. + It can be of one of these values: default,google,azure,amazon,github. + items: + description: ImageRegistryCredentialsProvidersType + provides the list of credential providers required. + enum: + - default + - amazon + - azure + - google + - github + type: string + type: array + secrets: + description: |- + Secrets specifies a list of secrets that are provided for credentials. + Secrets must live in the Kyverno namespace. + items: + type: string + type: array + type: object + jmesPath: + description: |- + JMESPath is an optional JSON Match Expression that can be used to + transform the ImageData struct returned as a result of processing + the image reference. + type: string + reference: + description: |- + Reference is image reference to a container image in the registry. + Example: ghcr.io/kyverno/kyverno:latest + type: string + required: + - reference + type: object + name: + description: Name is the variable name. + type: string + variable: + description: Variable defines an arbitrary JMESPath context + variable that can be defined inline. + properties: + default: + description: |- + Default is an optional arbitrary JSON object that the variable may take if the JMESPath + expression evaluates to nil + x-kubernetes-preserve-unknown-fields: true + jmesPath: + description: |- + JMESPath is an optional JMESPath Expression that can be used to + transform the variable. + type: string + value: + description: Value is any arbitrary JSON object representable + in YAML or JSON form. + x-kubernetes-preserve-unknown-fields: true + type: object + required: + - name + type: object + type: array + exclude: + description: |- + ExcludeResources defines when cleanuppolicy should not be applied. The exclude + criteria can include resource information (e.g. kind, name, namespace, labels) + and admission review request information like the name or role. + not: + required: + - any + - all + properties: + all: + description: All allows specifying resources which will be ANDed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + any: + description: Any allows specifying resources which will be ORed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + type: object + match: + description: |- + MatchResources defines when cleanuppolicy should be applied. The match + criteria can include resource information (e.g. kind, name, namespace, labels) + and admission review request information like the user name or role. + At least one kind is required. + not: + required: + - any + - all + properties: + all: + description: All allows specifying resources which will be ANDed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + any: + description: Any allows specifying resources which will be ORed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + type: object + schedule: + description: The schedule in Cron format + type: string + required: + - match + - schedule + type: object + status: + description: Status contains policy runtime data. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastExecutionTime: + format: date-time + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.schedule + name: Schedule + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + name: v2beta1 + schema: + openAPIV3Schema: + description: ClusterCleanupPolicy defines rule for resource cleanup. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec declares policy behaviors. + properties: + conditions: + description: Conditions defines the conditions used to select the + resources which will be cleaned up. + properties: + all: + description: |- + AllConditions enable variable-based conditional rule execution. This is useful for + finer control of when an rule is applied. A condition can reference object data + using JMESPath notation. + Here, all of the conditions need to pass. + items: + properties: + key: + description: Key is the context entry (using JMESPath) for + conditional rule evaluation. + x-kubernetes-preserve-unknown-fields: true + message: + description: Message is an optional display message + type: string + operator: + description: |- + Operator is the conditional operation to perform. Valid operators are: + Equals, NotEquals, In, AnyIn, AllIn, NotIn, AnyNotIn, AllNotIn, GreaterThanOrEquals, + GreaterThan, LessThanOrEquals, LessThan, DurationGreaterThanOrEquals, DurationGreaterThan, + DurationLessThanOrEquals, DurationLessThan + enum: + - Equals + - NotEquals + - AnyIn + - AllIn + - AnyNotIn + - AllNotIn + - GreaterThanOrEquals + - GreaterThan + - LessThanOrEquals + - LessThan + - DurationGreaterThanOrEquals + - DurationGreaterThan + - DurationLessThanOrEquals + - DurationLessThan + type: string + value: + description: |- + Value is the conditional value, or set of values. The values can be fixed set + or can be variables declared using JMESPath. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + any: + description: |- + AnyConditions enable variable-based conditional rule execution. This is useful for + finer control of when an rule is applied. A condition can reference object data + using JMESPath notation. + Here, at least one of the conditions need to pass. + items: + properties: + key: + description: Key is the context entry (using JMESPath) for + conditional rule evaluation. + x-kubernetes-preserve-unknown-fields: true + message: + description: Message is an optional display message + type: string + operator: + description: |- + Operator is the conditional operation to perform. Valid operators are: + Equals, NotEquals, In, AnyIn, AllIn, NotIn, AnyNotIn, AllNotIn, GreaterThanOrEquals, + GreaterThan, LessThanOrEquals, LessThan, DurationGreaterThanOrEquals, DurationGreaterThan, + DurationLessThanOrEquals, DurationLessThan + enum: + - Equals + - NotEquals + - AnyIn + - AllIn + - AnyNotIn + - AllNotIn + - GreaterThanOrEquals + - GreaterThan + - LessThanOrEquals + - LessThan + - DurationGreaterThanOrEquals + - DurationGreaterThan + - DurationLessThanOrEquals + - DurationLessThan + type: string + value: + description: |- + Value is the conditional value, or set of values. The values can be fixed set + or can be variables declared using JMESPath. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + type: object + context: + description: Context defines variables and data sources that can be + used during rule execution. + items: + description: |- + ContextEntry adds variables and data sources to a rule Context. Either a + ConfigMap reference or a APILookup must be provided. + oneOf: + - required: + - configMap + - required: + - apiCall + - required: + - imageRegistry + - required: + - variable + - required: + - globalReference + properties: + apiCall: + description: |- + APICall is an HTTP request to the Kubernetes API server, or other JSON web service. + The data returned is stored in the context with the name for the context entry. + properties: + data: + description: |- + The data object specifies the POST data sent to the server. + Only applicable when the method field is set to POST. + items: + description: RequestData contains the HTTP POST data + properties: + key: + description: Key is a unique identifier for the data + value + type: string + value: + description: Value is the data value + x-kubernetes-preserve-unknown-fields: true + required: + - key + - value + type: object + type: array + default: + description: |- + Default is an optional arbitrary JSON object that the context + value is set to, if the apiCall returns error. + x-kubernetes-preserve-unknown-fields: true + jmesPath: + description: |- + JMESPath is an optional JSON Match Expression that can be used to + transform the JSON response returned from the server. For example + a JMESPath of "items | length(@)" applied to the API server response + for the URLPath "/apis/apps/v1/deployments" will return the total count + of deployments across all namespaces. + type: string + method: + default: GET + description: Method is the HTTP request type (GET or POST). + Defaults to GET. + enum: + - GET + - POST + type: string + service: + description: |- + Service is an API call to a JSON web service. + This is used for non-Kubernetes API server calls. + It's mutually exclusive with the URLPath field. + properties: + caBundle: + description: |- + CABundle is a PEM encoded CA bundle which will be used to validate + the server certificate. + type: string + headers: + description: Headers is a list of optional HTTP headers + to be included in the request. + items: + properties: + key: + description: Key is the header key + type: string + value: + description: Value is the header value + type: string + required: + - key + - value + type: object + type: array + url: + description: |- + URL is the JSON web service URL. A typical form is + `https://{service}.{namespace}:{port}/{path}`. + type: string + required: + - url + type: object + urlPath: + description: |- + URLPath is the URL path to be used in the HTTP GET or POST request to the + Kubernetes API server (e.g. "/api/v1/namespaces" or "/apis/apps/v1/deployments"). + The format required is the same format used by the `kubectl get --raw` command. + See https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls + for details. + It's mutually exclusive with the Service field. + type: string + type: object + configMap: + description: ConfigMap is the ConfigMap reference. + properties: + name: + description: Name is the ConfigMap name. + type: string + namespace: + description: Namespace is the ConfigMap namespace. + type: string + required: + - name + type: object + globalReference: + description: GlobalContextEntryReference is a reference to a + cached global context entry. + properties: + jmesPath: + description: |- + JMESPath is an optional JSON Match Expression that can be used to + transform the JSON response returned from the server. For example + a JMESPath of "items | length(@)" applied to the API server response + for the URLPath "/apis/apps/v1/deployments" will return the total count + of deployments across all namespaces. + type: string + name: + description: Name of the global context entry + type: string + required: + - name + type: object + imageRegistry: + description: |- + ImageRegistry defines requests to an OCI/Docker V2 registry to fetch image + details. + properties: + imageRegistryCredentials: + description: ImageRegistryCredentials provides credentials + that will be used for authentication with registry + properties: + allowInsecureRegistry: + description: AllowInsecureRegistry allows insecure access + to a registry. + type: boolean + providers: + description: |- + Providers specifies a list of OCI Registry names, whose authentication providers are provided. + It can be of one of these values: default,google,azure,amazon,github. + items: + description: ImageRegistryCredentialsProvidersType + provides the list of credential providers required. + enum: + - default + - amazon + - azure + - google + - github + type: string + type: array + secrets: + description: |- + Secrets specifies a list of secrets that are provided for credentials. + Secrets must live in the Kyverno namespace. + items: + type: string + type: array + type: object + jmesPath: + description: |- + JMESPath is an optional JSON Match Expression that can be used to + transform the ImageData struct returned as a result of processing + the image reference. + type: string + reference: + description: |- + Reference is image reference to a container image in the registry. + Example: ghcr.io/kyverno/kyverno:latest + type: string + required: + - reference + type: object + name: + description: Name is the variable name. + type: string + variable: + description: Variable defines an arbitrary JMESPath context + variable that can be defined inline. + properties: + default: + description: |- + Default is an optional arbitrary JSON object that the variable may take if the JMESPath + expression evaluates to nil + x-kubernetes-preserve-unknown-fields: true + jmesPath: + description: |- + JMESPath is an optional JMESPath Expression that can be used to + transform the variable. + type: string + value: + description: Value is any arbitrary JSON object representable + in YAML or JSON form. + x-kubernetes-preserve-unknown-fields: true + type: object + required: + - name + type: object + type: array + exclude: + description: |- + ExcludeResources defines when cleanuppolicy should not be applied. The exclude + criteria can include resource information (e.g. kind, name, namespace, labels) + and admission review request information like the name or role. + not: + required: + - any + - all + properties: + all: + description: All allows specifying resources which will be ANDed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + any: + description: Any allows specifying resources which will be ORed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + type: object + match: + description: |- + MatchResources defines when cleanuppolicy should be applied. The match + criteria can include resource information (e.g. kind, name, namespace, labels) + and admission review request information like the user name or role. + At least one kind is required. + not: + required: + - any + - all + properties: + all: + description: All allows specifying resources which will be ANDed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + any: + description: Any allows specifying resources which will be ORed + items: + description: ResourceFilter allow users to "AND" or "OR" between + resources + properties: + clusterRoles: + description: ClusterRoles is the list of cluster-wide role + names for the user. + items: + type: string + type: array + resources: + description: ResourceDescription contains information about + the resource being created or modified. + not: + required: + - name + - names + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is a map of annotations (key-value pairs of type string). Annotation keys + and values support the wildcard characters "*" (matches zero or many characters) and + "?" (matches at least one character). + type: object + kinds: + description: Kinds is a list of resource kinds. + items: + type: string + type: array + name: + description: |- + Name is the name of the resource. The name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + NOTE: "Name" is being deprecated in favor of "Names". + type: string + names: + description: |- + Names are the names of the resources. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + namespaceSelector: + description: |- + NamespaceSelector is a label selector for the resource namespace. Label keys and values + in `matchLabels` support the wildcard characters `*` (matches zero or many characters) + and `?` (matches one character).Wildcards allows writing label selectors like + ["storage.k8s.io/*": "*"]. Note that using ["*" : "*"] matches any key and value but + does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + Namespaces is a list of namespaces names. Each name supports wildcard characters + "*" (matches zero or many characters) and "?" (at least one character). + items: + type: string + type: array + operations: + description: Operations can contain values ["CREATE, + "UPDATE", "CONNECT", "DELETE"], which are used to + match a specific action. + items: + description: AdmissionOperation can have one of the + values CREATE, UPDATE, CONNECT, DELETE, which are + used to match a specific action. + enum: + - CREATE + - CONNECT + - UPDATE + - DELETE + type: string + type: array + selector: + description: |- + Selector is a label selector. Label keys and values in `matchLabels` support the wildcard + characters `*` (matches zero or many characters) and `?` (matches one character). + Wildcards allows writing label selectors like ["storage.k8s.io/*": "*"]. Note that + using ["*" : "*"] matches any key and value but does not match an empty label set. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + roles: + description: Roles is the list of namespaced role names + for the user. + items: + type: string + type: array + subjects: + description: Subjects is the list of subject names like + users, user groups, and service accounts. + items: + description: |- + Subject contains a reference to the object or user identities a role binding applies to. This can either hold a direct API object reference, + or a value for non-objects such as user and group names. + properties: + apiGroup: + description: |- + APIGroup holds the API group of the referenced subject. + Defaults to "" for ServiceAccount subjects. + Defaults to "rbac.authorization.k8s.io" for User and Group subjects. + type: string + kind: + description: |- + Kind of object being referenced. Values defined by this API group are "User", "Group", and "ServiceAccount". + If the Authorizer does not recognized the kind value, the Authorizer should report an error. + type: string + name: + description: Name of the object being referenced. + type: string + namespace: + description: |- + Namespace of the referenced object. If the object kind is non-namespace, such as "User" or "Group", and this value is not empty + the Authorizer should report an error. + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + type: array + type: object + type: array + type: object + schedule: + description: The schedule in Cron format + type: string + required: + - match + - schedule + type: object + status: + description: Status contains policy runtime data. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastExecutionTime: + format: date-time + type: string + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} diff --git a/internal/controller/testdata/sops/.sops.yaml b/internal/controller/testdata/sops/.sops.yaml new file mode 100644 index 000000000..313ea769b --- /dev/null +++ b/internal/controller/testdata/sops/.sops.yaml @@ -0,0 +1,30 @@ +stores: + json: + indent: 2 + yaml: + indent: 2 + +# creation rules are evaluated sequentially, the first match wins +creation_rules: + # Testing PGP + - path_regex: (inside|pgp)\.yaml$ + encrypted_regex: &encrypted_regex ^(data|stringData)$ + pgp: &pgp 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1 + + - path_regex: json\.yaml$ + encrypted_regex: ".*" + age: &age age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 + + - path_regex: \.yaml$ + encrypted_regex: *encrypted_regex + age: *age + + - path_regex: \.(env|txt)$ + age: *age + + # Fallback + - key_groups: + - age: + - *age + - pgp: + - *pgp diff --git a/internal/controller/testdata/sops/algorithms/age.yaml b/internal/controller/testdata/sops/algorithms/age.yaml new file mode 100644 index 000000000..6dfd8d7f0 --- /dev/null +++ b/internal/controller/testdata/sops/algorithms/age.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: age +stringData: + key: ENC[AES256_GCM,data:mHeXsmQ=,iv:vUMpILz3xchORqkzDFvgwENY7EqIHHGJdEF6C8xqbFE=,tag:IroV7hykADvD0IUaq6kikA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZeHVSdjJoY3ZSQjJzbk1q + ZXFxMWJ5amkrN1VXeHI4QzQ5OHcwVGxDem1zCm8wQVEzNEUrOUhtRUFkVnFUY0tN + aFgwaHNrWmVWY1RGWXI2YlpYbUhYMGMKLS0tIDBFSXo3cjRCMngvTXpldzhMRlVp + TXk2d2ExSVZYNDVTV0xwVlZnQnpScG8KVpjffjtRTA7Z4Wf/l1VMLjcl16hOrRUv + LKiZDcq+nqKDUI7owZ+xNs2w5SrQjEWVhDXRSeSSRiJrK/bCYKzRxA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-11-12T13:33:42Z" + mac: ENC[AES256_GCM,data:vmrF+VgW3o8z4h/DOStCUNudz68yHEC8Mws+LPoKpM3Xc7GM0Z1CfX0TKwdLLjMuvyWa2Nx2NIxm0+MCbmR8+y2izn0hHPSWhNVCWSK+iW48M05vXhDCV0xNkqM7g0kLhQ3PiSrB69loQj8C590HIfEViEtyDCFUeynDgcC289Q=,iv:u5lhmtXMxyt+3Pw09wWvgBhmKLoOSpKNWUpu/LuCr3Y=,tag:Dg0HFdLgQltzPgnEmltAzQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.0 diff --git a/internal/controller/testdata/sops/algorithms/kustomization.yaml b/internal/controller/testdata/sops/algorithms/kustomization.yaml new file mode 100644 index 000000000..be0ed2915 --- /dev/null +++ b/internal/controller/testdata/sops/algorithms/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: algo- +resources: + - age.yaml + - pgp.yaml + - vault.yaml diff --git a/internal/controller/testdata/sops/algorithms/pgp.yaml b/internal/controller/testdata/sops/algorithms/pgp.yaml new file mode 100644 index 000000000..2e8f4adb6 --- /dev/null +++ b/internal/controller/testdata/sops/algorithms/pgp.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pgp +stringData: + key: ENC[AES256_GCM,data:EJey73Q=,iv:QRdpZJ6WYi3fWpKwjl8ZiV+Wwq9qtYTpcMQ0j0OEa44=,tag:d1WlcRpwEJg1lk3X3ILDmA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: [] + lastmodified: "2024-11-12T13:33:42Z" + mac: ENC[AES256_GCM,data:25ERLClNe3o33jEo109QtmVH/qzl+e0pMRR1RDyQ4QHrVqYfMIvgUeYDHAIJ5WDwQaueON8nne1KIo+fcPYVBdHvTYvnZiicCUPA5/fpgbyts0u5CdUs31bltI/blnUlU8VbJfIk2Zjlj93erLw23sdzdo/0xsdDTrf3bYiS2CI=,iv:vxrgdyqIKRWGBA+dgrGbjGn7tkXEqbADayIxuzNwxp0=,tag:qWesJqClsLpZHY9UR7ptLQ==,type:str] + pgp: + - created_at: "2024-11-12T13:33:42Z" + enc: |- + -----BEGIN PGP MESSAGE----- + + hQIMA90SOJihaAjLARAAqSf7bnqHB0/gfh8CmweYr5cfUpH8aYg7B5QhsnD6nOok + x0UIPtaxtfEBvuDsM9M678Gj/hTEzMv0FmDYRt88NAXm1+63HHnz0/0O3xXQ/DR6 + +1uEZruuyC23nyzjc1fefaqgZ1YJAnj5WCvcWaF12bXbIdFQpRhpVcoMMqWhQizF + 5QJFXjU3cnzIVtvcpMDD63NTpk8+hSTYJr5ZFODSMbQr+EPHvKPMrIx3LLcihkkS + eyxvfLalj556f/3QVgGuOX6VX8lPIaUyIcmXyUkGsooEirOyhiZg2sk/QB6TYIa6 + Nm62hmeeXP01wyY6tax7l3LpAuda6CJRVg+Je1OkIjiuPMIBzHgtfhGFks8vgeTP + xsHXKLKXlJAQyS4ewOItm9n9jc9Xdnwfli4HrGbHNzq7lgEyAOyZZtOifl4KqFbM + 0c3kGiP3ezycRrQGudvbdIZqGfeD+gKrBv6cV49Wgt7Nb1WJUKLcPv4PNtSlYzSu + lGDM63bO+QBAKObc6MOvLnVXbFXrErLMqrexN9XFdjvvsmQAVr2z5phZk5fEk7kw + j8CqyTuy2Dm+ChJwNEeqIY3BNHkvvWMLx8Cr7ZY6bO1BvOdp01mBf+XD/apeBBUe + v2DT36mCehKZh5BHDYH7hKCNw+4PN2hzZd02zKMNzmARqLzQeseaTXti3Hyze23S + XAG1ddNzKXsgbTwLog5EN7DTIQKR+uCIgHuK0DclyWvTiUK7P6HGepTE7byJnnpl + jHtAVs8t+cYHBtY+gKFsstRGbJgAe8QfIt12/XMu9jcA/r8m7xdyNS5P9VZj + =gXAv + -----END PGP MESSAGE----- + fp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1 + encrypted_regex: ^(data|stringData)$ + version: 3.9.0 diff --git a/internal/controller/testdata/sops/algorithms/vault.yaml b/internal/controller/testdata/sops/algorithms/vault.yaml new file mode 100644 index 000000000..14d0b8bd6 --- /dev/null +++ b/internal/controller/testdata/sops/algorithms/vault.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: vault +stringData: + key: value diff --git a/internal/controller/testdata/sops/component/env.env b/internal/controller/testdata/sops/component/env.env new file mode 100644 index 000000000..1971d7229 --- /dev/null +++ b/internal/controller/testdata/sops/component/env.env @@ -0,0 +1,7 @@ +key=ENC[AES256_GCM,data:HfbmmMU=,iv:nWWqqIzzutZJBzu5PbaTPBsqvszaz2/+58mYOK7hj9Q=,tag:b+VcateAccwdb7x2dmYDrQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsc0Vyd25KTE1sYWM1akFH\nTUFBeHBmSmdGMnY3ZFJvazRZMUtPMFpscmhBCnVsL2Y0cUd1Nkx1Z0Q1OWpHOG0w\nNnhXSmxjbzR5NVE1NGpjR3d2SHN6SzgKLS0tIG5tdXpXK0U2SUlsQlcvY0ZvRWJB\nS2N6MS9QRVR4K2toMEg1eDR3a3ZtdzAKiliurqchsdfT4XbttES0ohnuTMNKlZy9\nefqbQO2lTLw8wUsNUunTpJBEAx9MFZ+LFHE/EZfHZqYlzxCPzfhufA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 +sops_lastmodified=2024-11-12T13:33:42Z +sops_mac=ENC[AES256_GCM,data:kPn8FhXF7UcPbkA7gjfjfYljawfT67SQBsYbnaAgtcFAtMWTryTHSDAASp2RZiClZiWnKgOgT8NeFUC+hUvjlz/Vj3pQxl6zY+3CmlrbBiqYUwd8ksXjps8UTqcioWKc7xULLqV5GMUHpoWnDWkkt0F6F10uCL78P0JoKmIeCXM=,iv:/G3GIGXriXuoS9OhfEazEYgVBbo+XvouTGYEi5XVYqQ=,tag:80P9IXhwJzoqJ43eK2W+4g==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.9.0 diff --git a/internal/controller/testdata/sops/component/kustomization.yaml b/internal/controller/testdata/sops/component/kustomization.yaml new file mode 100644 index 000000000..4bf4be0aa --- /dev/null +++ b/internal/controller/testdata/sops/component/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +generatorOptions: + disableNameSuffixHash: true +secretGenerator: + - name: component + envs: + - env.env diff --git a/internal/controller/testdata/sops/envs/env.env b/internal/controller/testdata/sops/envs/env.env new file mode 100644 index 000000000..792dc1a0f --- /dev/null +++ b/internal/controller/testdata/sops/envs/env.env @@ -0,0 +1,7 @@ +key=ENC[AES256_GCM,data:3PTvx6o=,iv:74ni7B2QMB6aygdd3R7IEzNCwo1W+TpPWMJLfYCCG4U=,tag:mK2Tu7JWDdEmZUrXz3uRzw==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5aDhVTW1IenNXQmptWnha\nMjd1UWN3dHp0QXRkSnhUSjBHVFdKSmdXYzNNClVWeXVGWndJQ1RpRUlJRy9yeHJY\nb1VhbnR2TlovSUg1MlpZdkhWdkVHTG8KLS0tIHVOSEhOVVV2cXRUQUs2Sk15eU1a\nRW92L1BWQnhNbStFekZjVVRDUFJtaWsK+wPkQAtZtTbh2WHik1ovX61ZJPpkmwuO\nnUYAn37tZELXX/alrOORRwoq+0oBQO5pZYsJBi0fvijfm9VqR/4jKg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 +sops_lastmodified=2024-11-12T13:33:42Z +sops_mac=ENC[AES256_GCM,data:YQHMLRk85ozeuqIvNekLAVp2DFSj+VgDG2z70uQaeCA+uxFp3k/THlANAXx+GP1Oab923Q6nG5ItV9dcG1hTXpA/NRpbM02pfNe/iYnVL7AtcXqFg/jy2T4kkqx7cHAXJi9zd+ZrISIZCNWinLoFfaAo70+epsFumUmLUaDzUPQ=,iv:TdOIRoy6Wch1/x9GlEsmArA5g461ILJZUE7tIxi9G28=,tag:miip/H0SuHqvaoxGvzheIg==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.9.0 diff --git a/controllers/testdata/test-dotenv/bases/kustomization.yaml b/internal/controller/testdata/sops/envs/kustomization.yaml similarity index 53% rename from controllers/testdata/test-dotenv/bases/kustomization.yaml rename to internal/controller/testdata/sops/envs/kustomization.yaml index 1c2a824cd..0299aab3b 100644 --- a/controllers/testdata/test-dotenv/bases/kustomization.yaml +++ b/internal/controller/testdata/sops/envs/kustomization.yaml @@ -1,8 +1,13 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -secretGenerator: - - name: sops-year2 - envs: - - ./secrets/year2.txt +namePrefix: envs- generatorOptions: disableNameSuffixHash: true +secretGenerator: +- name: secret + envs: + - env.env +configMapGenerator: +- name: configmap + envs: + - env.env diff --git a/internal/controller/testdata/sops/files/file.txt b/internal/controller/testdata/sops/files/file.txt new file mode 100644 index 000000000..367d76d9b --- /dev/null +++ b/internal/controller/testdata/sops/files/file.txt @@ -0,0 +1,20 @@ +{ + "data": "ENC[AES256_GCM,data:QNbPAYY=,iv:cMvqZZXqOFmH+bAFdzX+ORH3cnj2cgKX/f6+8q8bDlA=,tag:Pb5wsv4wq5mbccaUhjqQCA==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAybkpYNFFjVFprQndmWklK\nVnpyVzFjRGZ5cU5IK1NHb2t6bjhKUnZVZ24wCnZFSjBrVEJ6RmpORGMrVHRWUXA5\nL1BMbk1jWXM2aGpVcTkzckdHYm14SmMKLS0tIDdBS2NGaWFWRlZvRktPYksvd0pa\nRzFBRWtHcXlWcVkvK0VKQVRPRGFlYXcKeSgCitkcDxVNZSxS/TsR72xVh6iPL4l5\nS+FP0R0wbo3LbunScvF168f4NhB5HRpS29a5onxH64HEiYdMitV8WA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2024-11-12T13:33:42Z", + "mac": "ENC[AES256_GCM,data:8H24g0IjdODRma+52utYPlZnGEH+Oi3LiXel2JExHEd1YwbBL417lTbJpZVIfwk7+SYLWw6V4ZbPgHFUHchhRH5URNqb4I0m/FhTMyDW2h0Zm1kM1zMdE8AZTGUyNhmVkrlw7GnBwuGwWS6Usm9C9XD5O+/2Yn20YqmB2/T3a0o=,iv:0sclmOePSOpekgQLr/kNTM2xKdr7djHn2xYSNrFSGD4=,tag:6gvdsQKSqKafO6VrXqlaeA==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.9.0" + } +} \ No newline at end of file diff --git a/controllers/testdata/sops/month/kustomization.yaml b/internal/controller/testdata/sops/files/kustomization.yaml similarity index 50% rename from controllers/testdata/sops/month/kustomization.yaml rename to internal/controller/testdata/sops/files/kustomization.yaml index 3d3668f79..d66fc8378 100644 --- a/controllers/testdata/sops/month/kustomization.yaml +++ b/internal/controller/testdata/sops/files/kustomization.yaml @@ -1,14 +1,13 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -secretGenerator: -- name: sops-month - files: - - month.yaml -- name: sops-year - envs: - - year.env -- name: unencrypted-sops-year - envs: - - unencrypted-year.env +namePrefix: files- generatorOptions: disableNameSuffixHash: true +secretGenerator: +- name: secret + files: + - key=file.txt +configMapGenerator: +- name: configmap + files: + - key=file.txt diff --git a/internal/controller/testdata/sops/inside/kustomization.yaml b/internal/controller/testdata/sops/inside/kustomization.yaml new file mode 100644 index 000000000..32eac275b --- /dev/null +++ b/internal/controller/testdata/sops/inside/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: inside- +resources: + - secret.yaml diff --git a/internal/controller/testdata/sops/inside/secret.yaml b/internal/controller/testdata/sops/inside/secret.yaml new file mode 100644 index 000000000..40d65cf89 --- /dev/null +++ b/internal/controller/testdata/sops/inside/secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret +data: + key: ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpySEx3RWdnPSxpdjo4eU1USXFyMnhYUTVZWXlxTm5PWGx6SDNJa0dpY2h5RFpCWlMxbS9EWitNPSx0YWc6eHpZQjUrYjlUSlcrdkpzU0xha1hwUT09LHR5cGU6c3RyXSIsCgkic29wcyI6IHsKCQkia21zIjogbnVsbCwKCQkiZ2NwX2ttcyI6IG51bGwsCgkJImF6dXJlX2t2IjogbnVsbCwKCQkiaGNfdmF1bHQiOiBudWxsLAoJCSJhZ2UiOiBbCgkJCXsKCQkJCSJyZWNpcGllbnQiOiAiYWdlMWw0NHhjbmc4ZHFqMzJubHY2ZDkzMHF2dnJueTA1aGdsemN2OXFwYzdreGpjNjkwMm1hNHF1ZnlzMjkiLAoJCQkJImVuYyI6ICItLS0tLUJFR0lOIEFHRSBFTkNSWVBURUQgRklMRS0tLS0tXG5ZV2RsTFdWdVkzSjVjSFJwYjI0dWIzSm5MM1l4Q2kwK0lGZ3lOVFV4T1NBelExVnJVek5UT1UxWU1VbzRNWHBxXG5NMHh4YUhOS2NqQlJOMlZxTUd0dE5HY3pVbUo1TVdwUGQxVXdDazAzYWxwdlNFY3haM0JOVkUxMlQwTTBNR3BFXG5hU3NyV0hsWVZsQkJUbGhzZFhkcVVrOVlVVGRDWjJjS0xTMHRJRGN3ZVZwbmF6ZFhORzlPVWxwSU1pdFVWMnBoXG5SRzExUkdsbWJTOU5aMGxETnpSNVdXNXVORTlITVc4S1FCSlJIRVlhSFphdlN1RUlCN3pDU3hCN0RSdkxoRW8xXG5vRlVVNjRWOW54RC80ZUQyVyswdWpFYnprbVJ1ZDhsdGo5clhSY1lyU3B3MVdXcTdRbjFlakE9PVxuLS0tLS1FTkQgQUdFIEVOQ1JZUFRFRCBGSUxFLS0tLS1cbiIKCQkJfQoJCV0sCgkJImxhc3Rtb2RpZmllZCI6ICIyMDI0LTExLTEyVDEzOjMyOjA0WiIsCgkJIm1hYyI6ICJFTkNbQUVTMjU2X0dDTSxkYXRhOjBLdzFwNmNPUFd2NTlvZXhRVTFVV0tMcjNvY3NLQ1dxWE1RUnFsMnlaazRiWGpvcm14NS9VcmNqbkF3NUZnRVQzWXRtT0p0cUpVWnRTUDRuRTkxMFUxZFd2bWhUL240cmRFMFpyYU5NQmpTbVMzUVVCcm1aeXJERDN0ODlhaUJBVE42MnlSZ0pVNmlWWU80bHdlb0VuWUQ0K09ZcWczWHBSNldUUjRkVEtwTT0saXY6aHliWHI2YVJIQWxTcFAwZXZQaEhaNWhpazlObGgvRFJEUEZRUklXTktyaz0sdGFnOjNybHhzbktuN08vM3RnalBNbm91bWc9PSx0eXBlOnN0cl0iLAoJCSJwZ3AiOiBudWxsLAoJCSJ1bmVuY3J5cHRlZF9zdWZmaXgiOiAiX3VuZW5jcnlwdGVkIiwKCQkidmVyc2lvbiI6ICIzLjkuMCIKCX0KfQ== diff --git a/controllers/testdata/sops/age.txt b/internal/controller/testdata/sops/keys/age.txt similarity index 100% rename from controllers/testdata/sops/age.txt rename to internal/controller/testdata/sops/keys/age.txt diff --git a/controllers/testdata/sops/pgp.asc b/internal/controller/testdata/sops/keys/pgp.asc similarity index 100% rename from controllers/testdata/sops/pgp.asc rename to internal/controller/testdata/sops/keys/pgp.asc diff --git a/internal/controller/testdata/sops/kustomization.yaml b/internal/controller/testdata/sops/kustomization.yaml new file mode 100644 index 000000000..9017c48cd --- /dev/null +++ b/internal/controller/testdata/sops/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: sops- +resources: + - algorithms + - envs + - files + - patches + - inside + - remote +components: + - ./component diff --git a/controllers/testdata/test-dotenv/overlays/kustomization.yaml b/internal/controller/testdata/sops/patches/kustomization.yaml similarity index 51% rename from controllers/testdata/test-dotenv/overlays/kustomization.yaml rename to internal/controller/testdata/sops/patches/kustomization.yaml index fae4d26e0..eb9e45e94 100644 --- a/controllers/testdata/test-dotenv/overlays/kustomization.yaml +++ b/internal/controller/testdata/sops/patches/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -resources: - - ../bases -secretGenerator: - - name: sops-year1 - envs: - - year1.env +namePrefix: patches- +patches: + - path: merge1.yaml + - path: merge2.yaml generatorOptions: disableNameSuffixHash: true +secretGenerator: + - name: secret + literals: + - key=value diff --git a/internal/controller/testdata/sops/patches/merge1.yaml b/internal/controller/testdata/sops/patches/merge1.yaml new file mode 100644 index 000000000..851563015 --- /dev/null +++ b/internal/controller/testdata/sops/patches/merge1.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret +stringData: + key: ENC[AES256_GCM,data:P7HTaDel,iv:YyIVQyWQpW5tEIGOsWRx6kFIP49Ciej60a5EccQg1us=,tag:Rg+MWSVit7f6dVSPLfoFOA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBicityZGExUWdURjJmaUdY + NDF1czNNZ1B0OWFPTGpGblNwZGpza2NPZ1RjCnhQcE55VDNOaVlCUG0reE5LeEtD + TzZJR0o1dUJlb2dqV2YwaGhWZEdGYVEKLS0tIFJsc054RHJMQTUxdm9MNTJmb3o5 + QVd5VkxJam5RT3RjNzdaN3NzYWtGV1kKaaKPbN6o9/XunC7KimHAXbg3iI29hg71 + VHeuzfLjhuwOJv/rlNyHIdqbvGlMHUU5exZ7dVr4DMen+FsNRvnfJg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-11-12T13:33:42Z" + mac: ENC[AES256_GCM,data:ArD1tNf9Z72ZyUXj7PiBbHDTbmhprOfp8UUFPE7z9O/WvHOCgfwfhtnDfri/SeHiKyLHVQjdvoEw+Xu9xCNkG+UJuKnz/YBT4Wq+jkbQTSOvFNL4K8HwroWmTmcKS2CVUy5N2U64qNg29nFceiMoX8mSvlqOLKMWLCPhYP4L3sc=,iv:hj4VEh3mWjD2NNE9aGG3rqw1niFfE3VTkgUpY2SwhA0=,tag:nVG2dca/11vDANi9Bgk3dA==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.0 diff --git a/internal/controller/testdata/sops/patches/merge2.yaml b/internal/controller/testdata/sops/patches/merge2.yaml new file mode 100644 index 000000000..9b7a443ae --- /dev/null +++ b/internal/controller/testdata/sops/patches/merge2.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret +stringData: + merge2: ENC[AES256_GCM,data:QN7wGPNK,iv:cg3UYtCAWmxxLMGvK3ImXz1j/kN0vyujQNzbJE84LCU=,tag:LwQwsEEam96wmeSwRmZevQ==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvTDFGM0pxZXc1VWQzWm8z + MGxYRWprMXFWakdiTmpycDB4RnBlc0lkUEJZCmlLQ0Q1a1BRcXQ5Q1ZpRGljM2Fn + SWlQaUVuUjNKb3p2NmYrdWxlUDIzajQKLS0tIGlZWUlQK05wOGVlRGp3UE5YalNZ + S1hNbFd5a1Q0KzNwOE1oa3JZUnRMdmMKg7Ac1ik+6gmtKF7SUkiGb/Prh3kyJUA6 + PlVtWc+QGanN7mkXIxnPbhoDF8RYrxXH0mot9iiFWdzH+IeC19DANA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-11-12T13:33:42Z" + mac: ENC[AES256_GCM,data:Lnz+0hdARiP6yHgyJugrtuuhKhy21X4TBQG3Pz0EVZWFfIfheWBbW9KOXlw+x7FruuGWQxIlMmmgCMx4YVxQwpT6zFvjUw6hfD4fpeyrxnsCOiN56N3ECpLZMfq27ilubnMHe/AC0mhdAjivZfQJWPe/lQBO3Jb6HRJj7FTPWWA=,iv:0mNU7QFsYCsxNvbtcPLg19dktr9eWDGQLcKw+WWCaFU=,tag:zp+dyySRJMjwccw4TEGnjg==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.0 diff --git a/internal/controller/testdata/sops/remote/env.env b/internal/controller/testdata/sops/remote/env.env new file mode 100644 index 000000000..792dc1a0f --- /dev/null +++ b/internal/controller/testdata/sops/remote/env.env @@ -0,0 +1,7 @@ +key=ENC[AES256_GCM,data:3PTvx6o=,iv:74ni7B2QMB6aygdd3R7IEzNCwo1W+TpPWMJLfYCCG4U=,tag:mK2Tu7JWDdEmZUrXz3uRzw==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5aDhVTW1IenNXQmptWnha\nMjd1UWN3dHp0QXRkSnhUSjBHVFdKSmdXYzNNClVWeXVGWndJQ1RpRUlJRy9yeHJY\nb1VhbnR2TlovSUg1MlpZdkhWdkVHTG8KLS0tIHVOSEhOVVV2cXRUQUs2Sk15eU1a\nRW92L1BWQnhNbStFekZjVVRDUFJtaWsK+wPkQAtZtTbh2WHik1ovX61ZJPpkmwuO\nnUYAn37tZELXX/alrOORRwoq+0oBQO5pZYsJBi0fvijfm9VqR/4jKg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29 +sops_lastmodified=2024-11-12T13:33:42Z +sops_mac=ENC[AES256_GCM,data:YQHMLRk85ozeuqIvNekLAVp2DFSj+VgDG2z70uQaeCA+uxFp3k/THlANAXx+GP1Oab923Q6nG5ItV9dcG1hTXpA/NRpbM02pfNe/iYnVL7AtcXqFg/jy2T4kkqx7cHAXJi9zd+ZrISIZCNWinLoFfaAo70+epsFumUmLUaDzUPQ=,iv:TdOIRoy6Wch1/x9GlEsmArA5g461ILJZUE7tIxi9G28=,tag:miip/H0SuHqvaoxGvzheIg==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.9.0 diff --git a/internal/controller/testdata/sops/remote/kustomization.yaml b/internal/controller/testdata/sops/remote/kustomization.yaml new file mode 100644 index 000000000..41788e42a --- /dev/null +++ b/internal/controller/testdata/sops/remote/kustomization.yaml @@ -0,0 +1,24 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namePrefix: remote- +resources: + - https://raw.githubusercontent.com/fluxcd/kustomize-controller/refs/heads/main/config/default/namespace.yaml +generatorOptions: + disableNameSuffixHash: true +secretGenerator: +- name: secret + envs: + - env.env +patches: + - patch: |- + apiVersion: v1 + kind: ConfigMap + metadata: + name: sops-remote-configmap + data: + key: value + target: + kind: Namespace + options: + allowNameChange: true + allowKindChange: true diff --git a/controllers/testdata/transformers/deployment.yaml b/internal/controller/testdata/transformers/deployment.yaml similarity index 100% rename from controllers/testdata/transformers/deployment.yaml rename to internal/controller/testdata/transformers/deployment.yaml diff --git a/controllers/testdata/transformers/kustomization.yaml b/internal/controller/testdata/transformers/kustomization.yaml similarity index 100% rename from controllers/testdata/transformers/kustomization.yaml rename to internal/controller/testdata/transformers/kustomization.yaml diff --git a/controllers/testdata/transformers/patch.yaml b/internal/controller/testdata/transformers/patch.yaml similarity index 100% rename from controllers/testdata/transformers/patch.yaml rename to internal/controller/testdata/transformers/patch.yaml diff --git a/controllers/testdata/transformers/quota.yaml b/internal/controller/testdata/transformers/quota.yaml similarity index 100% rename from controllers/testdata/transformers/quota.yaml rename to internal/controller/testdata/transformers/quota.yaml diff --git a/controllers/utils.go b/internal/controller/utils.go similarity index 76% rename from controllers/utils.go rename to internal/controller/utils.go index d2363ac77..c186fdec6 100644 --- a/controllers/utils.go +++ b/internal/controller/utils.go @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controller import ( "fmt" "os" "path/filepath" + + "github.com/fluxcd/pkg/ssa" ) // MkdirTempAbs creates a tmp dir and returns the absolute path to the dir. @@ -36,3 +38,16 @@ func MkdirTempAbs(dir, pattern string) (string, error) { } return tmpDir, nil } + +// HasChanged evaluates the given action and returns true +// if the action type matches a resource mutation or deletion. +func HasChanged(action ssa.Action) bool { + switch action { + case ssa.SkippedAction: + return false + case ssa.UnchangedAction: + return false + default: + return true + } +} diff --git a/internal/decryptor/decryptor.go b/internal/decryptor/decryptor.go index 45a951253..970a9d506 100644 --- a/internal/decryptor/decryptor.go +++ b/internal/decryptor/decryptor.go @@ -25,17 +25,29 @@ import ( "io/fs" "os" "path/filepath" - "sort" "strings" "sync" "time" + gcpkmsapi "cloud.google.com/go/kms/apiv1" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + awssdk "github.com/aws/aws-sdk-go-v2/aws" securejoin "github.com/cyphar/filepath-securejoin" - "go.mozilla.org/sops/v3" - "go.mozilla.org/sops/v3/aes" - "go.mozilla.org/sops/v3/cmd/sops/common" - "go.mozilla.org/sops/v3/cmd/sops/formats" - "go.mozilla.org/sops/v3/keyservice" + "github.com/fluxcd/pkg/auth" + "github.com/fluxcd/pkg/auth/aws" + "github.com/fluxcd/pkg/auth/azure" + "github.com/fluxcd/pkg/auth/gcp" + "github.com/fluxcd/pkg/cache" + "github.com/getsops/sops/v3" + "github.com/getsops/sops/v3/aes" + "github.com/getsops/sops/v3/age" + "github.com/getsops/sops/v3/cmd/sops/common" + "github.com/getsops/sops/v3/cmd/sops/formats" + "github.com/getsops/sops/v3/config" + "github.com/getsops/sops/v3/keyservice" + "github.com/getsops/sops/v3/pgp" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -46,12 +58,11 @@ import ( kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - "github.com/fluxcd/kustomize-controller/internal/sops/age" - "github.com/fluxcd/kustomize-controller/internal/sops/awskms" - "github.com/fluxcd/kustomize-controller/internal/sops/azkv" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + intcache "github.com/fluxcd/kustomize-controller/internal/cache" + intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms" + intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv" intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice" - "github.com/fluxcd/kustomize-controller/internal/sops/pgp" ) const ( @@ -105,8 +116,7 @@ var ( } ) -// Decryptor performs decryption operations for a -// v1beta2.Kustomization. +// Decryptor performs decryption operations for a v1.Kustomization. // The only supported decryption provider at present is // DecryptionProviderSOPS. type Decryptor struct { @@ -115,8 +125,8 @@ type Decryptor struct { root string // client is the Kubernetes client used to e.g. retrieve Secrets with. client client.Client - // kustomization is the v1beta2.Kustomization we are decrypting for. - // The v1beta2.Decryption of the object is used to ImportKeys(). + // kustomization is the v1.Kustomization we are decrypting for. + // The v1.Decryption of the object is used to ImportKeys(). kustomization *kustomizev1.Kustomization // maxFileSize is the max size in bytes a file is allowed to have to be // decrypted. Defaults to maxEncryptedFileSize. @@ -126,6 +136,8 @@ type Decryptor struct { // injected into most resources, causing the integrity check to fail. // Mostly kept around for feature completeness and documentation purposes. checkSopsMac bool + // tokenCache is the cache for token credentials. + tokenCache *cache.TokenCache // gnuPGHome is the absolute path of the GnuPG home directory used to // decrypt PGP data. When empty, the systems' GnuPG keyring is used. @@ -136,15 +148,15 @@ type Decryptor struct { // vaultToken is the Hashicorp Vault token used to authenticate towards // any Vault server. vaultToken string - // awsCredsProvider is the AWS credentials provider object used to authenticate + // awsCredentialsProvider is the AWS credentials provider object used to authenticate // towards any AWS KMS. - awsCredsProvider *awskms.CredsProvider - // azureToken is the Azure credential token used to authenticate towards + awsCredentialsProvider func(region string) awssdk.CredentialsProvider + // azureTokenCredential is the Azure credential token used to authenticate towards // any Azure Key Vault. - azureToken *azkv.Token - // gcpCredsJSON is the JSON credential file of the service account used to - // authenticate towards any GCP KMS. - gcpCredsJSON []byte + azureTokenCredential azcore.TokenCredential + // gcpTokenSource is the GCP token source used to authenticate towards + // any GCP KMS. + gcpTokenSource oauth2.TokenSource // keyServices are the SOPS keyservice.KeyServiceClient's available to the // decryptor. @@ -154,25 +166,28 @@ type Decryptor struct { // NewDecryptor creates a new Decryptor for the given kustomization. // gnuPGHome can be empty, in which case the systems' keyring is used. -func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization, maxFileSize int64, gnuPGHome string) *Decryptor { +func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization, + maxFileSize int64, gnuPGHome string, tokenCache *cache.TokenCache) *Decryptor { return &Decryptor{ root: root, client: client, kustomization: kustomization, maxFileSize: maxFileSize, gnuPGHome: pgp.GnuPGHome(gnuPGHome), + tokenCache: tokenCache, } } // NewTempDecryptor creates a new Decryptor, with a temporary GnuPG // home directory to Decryptor.ImportKeys() into. -func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization) (*Decryptor, func(), error) { +func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization, + tokenCache *cache.TokenCache) (*Decryptor, func(), error) { gnuPGHome, err := pgp.NewGnuPGHome() if err != nil { return nil, nil, fmt.Errorf("cannot create decryptor: %w", err) } cleanup := func() { _ = os.RemoveAll(gnuPGHome.String()) } - return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String()), cleanup, nil + return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String(), tokenCache), cleanup, nil } // IsEncryptedSecret checks if the given object is a Kubernetes Secret encrypted @@ -187,7 +202,7 @@ func IsEncryptedSecret(object *unstructured.Unstructured) bool { } // ImportKeys imports the DecryptionProviderSOPS keys from the data values of -// the Secret referenced in the Kustomization's v1beta2.Decryption spec. +// the Secret referenced in the Kustomization's v1.Decryption spec. // It returns an error if the Secret cannot be retrieved, or if one of the // imports fails. // Imports do not have an effect after the first call to SopsDecryptWithFormat(), @@ -227,7 +242,6 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error { return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) } case filepath.Ext(DecryptionVaultTokenFileName): - // Make sure we have the absolute name if name == DecryptionVaultTokenFileName { token := string(value) token = strings.Trim(strings.TrimSpace(token), "\n") @@ -235,24 +249,32 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error { } case filepath.Ext(DecryptionAWSKmsFile): if name == DecryptionAWSKmsFile { - if d.awsCredsProvider, err = awskms.LoadCredsProviderFromYaml(value); err != nil { + awsCreds, err := intawskms.LoadStaticCredentialsFromYAML(value) + if err != nil { return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) } + d.awsCredentialsProvider = func(string) awssdk.CredentialsProvider { return awsCreds } } case filepath.Ext(DecryptionAzureAuthFile): - // Make sure we have the absolute name if name == DecryptionAzureAuthFile { - conf := azkv.AADConfig{} - if err = azkv.LoadAADConfigFromBytes(value, &conf); err != nil { + conf := intazkv.AADConfig{} + if err = intazkv.LoadAADConfigFromBytes(value, &conf); err != nil { return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) } - if d.azureToken, err = azkv.TokenFromAADConfig(conf); err != nil { + azureToken, err := intazkv.TokenCredentialFromAADConfig(conf) + if err != nil { return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) } + d.azureTokenCredential = azureToken } case filepath.Ext(DecryptionGCPCredsFile): if name == DecryptionGCPCredsFile { - d.gcpCredsJSON = bytes.Trim(value, "\n") + creds, err := google.CredentialsFromJSON(ctx, + bytes.Trim(value, "\n"), gcpkmsapi.DefaultAuthScopes()...) + if err != nil { + return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) + } + d.gcpTokenSource = creds.TokenSource } } } @@ -260,6 +282,63 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error { return nil } +// SetAuthOptions sets the authentication options for secret-less authentication +// with cloud providers. +func (d *Decryptor) SetAuthOptions(ctx context.Context) { + if d.kustomization.Spec.Decryption == nil { + return + } + + switch d.kustomization.Spec.Decryption.Provider { + case DecryptionProviderSOPS: + var opts []auth.Option + + if d.kustomization.Spec.Decryption.ServiceAccountName != "" { + serviceAccount := types.NamespacedName{ + Name: d.kustomization.Spec.Decryption.ServiceAccountName, + Namespace: d.kustomization.GetNamespace(), + } + opts = append(opts, auth.WithServiceAccount(serviceAccount, d.client)) + } + + involvedObject := cache.InvolvedObject{ + Kind: kustomizev1.KustomizationKind, + Name: d.kustomization.GetName(), + Namespace: d.kustomization.GetNamespace(), + } + + if d.awsCredentialsProvider == nil { + awsOpts := opts + if d.tokenCache != nil { + involvedObject.Operation = intcache.OperationDecryptWithAWS + awsOpts = append(awsOpts, auth.WithCache(*d.tokenCache, involvedObject)) + } + d.awsCredentialsProvider = func(region string) awssdk.CredentialsProvider { + awsOpts := append(awsOpts, auth.WithSTSRegion(region)) + return aws.NewCredentialsProvider(ctx, awsOpts...) + } + } + + if d.azureTokenCredential == nil { + azureOpts := opts + if d.tokenCache != nil { + involvedObject.Operation = intcache.OperationDecryptWithAzure + azureOpts = append(azureOpts, auth.WithCache(*d.tokenCache, involvedObject)) + } + d.azureTokenCredential = azure.NewTokenCredential(ctx, azureOpts...) + } + + if d.gcpTokenSource == nil { + gcpOpts := opts + if d.tokenCache != nil { + involvedObject.Operation = intcache.OperationDecryptWithGCP + gcpOpts = append(gcpOpts, auth.WithCache(*d.tokenCache, involvedObject)) + } + d.gcpTokenSource = gcp.NewTokenSource(ctx, gcpOpts...) + } + } +} + // SopsDecryptWithFormat attempts to load a SOPS encrypted file using the store // for the input format, gathers the data key for it from the key service, // and then decrypts the file data with the retrieved data key. @@ -274,27 +353,20 @@ func (d *Decryptor) SopsDecryptWithFormat(data []byte, inputFormat, outputFormat } }() - store := common.StoreForFormat(inputFormat) + store := common.StoreForFormat(inputFormat, config.NewStoresConfig()) tree, err := store.LoadEncryptedFile(data) if err != nil { return nil, sopsUserErr(fmt.Sprintf("failed to load encrypted %s data", sopsFormatToString[inputFormat]), err) } - for _, group := range tree.Metadata.KeyGroups { - // Sort MasterKeys in the group so offline ones are tried first - sort.SliceStable(group, func(i, j int) bool { - return intkeyservice.IsOfflineMethod(group[i]) && !intkeyservice.IsOfflineMethod(group[j]) - }) - } - - metadataKey, err := tree.Metadata.GetDataKeyWithKeyServices(d.keyServiceServer()) + metadataKey, err := tree.Metadata.GetDataKeyWithKeyServices(d.keyServiceServer(), sops.DefaultDecryptionOrder) if err != nil { return nil, sopsUserErr("cannot get sops data key", err) } cipher := aes.NewCipher() - mac, err := tree.Decrypt(metadataKey, cipher) + mac, err := safeDecrypt(tree.Decrypt(metadataKey, cipher)) if err != nil { return nil, sopsUserErr("error decrypting sops tree", err) } @@ -303,12 +375,12 @@ func (d *Decryptor) SopsDecryptWithFormat(data []byte, inputFormat, outputFormat // Compute the hash of the cleartext tree and compare it with // the one that was stored in the document. If they match, // integrity was preserved - // Ref: go.mozilla.org/sops/v3/decrypt/decrypt.go - originalMac, err := cipher.Decrypt( + // Ref: github.com/getsops/sops/v3/decrypt/decrypt.go + originalMac, err := safeDecrypt(cipher.Decrypt( tree.Metadata.MessageAuthenticationCode, metadataKey, tree.Metadata.LastModified.Format(time.RFC3339), - ) + )) if err != nil { return nil, sopsUserErr("failed to verify sops data integrity", err) } @@ -321,7 +393,7 @@ func (d *Decryptor) SopsDecryptWithFormat(data []byte, inputFormat, outputFormat } } - outputStore := common.StoreForFormat(outputFormat) + outputStore := common.StoreForFormat(outputFormat, config.NewStoresConfig()) out, err := outputStore.EmitPlainFile(tree.Branches) if err != nil { return nil, sopsUserErr(fmt.Sprintf("failed to emit encrypted %s file as decrypted %s", @@ -394,28 +466,28 @@ func (d *Decryptor) DecryptResource(res *resource.Resource) (*resource.Resource, return nil, nil } -// DecryptEnvSources attempts to decrypt all types.SecretArgs FileSources and +// DecryptSources attempts to decrypt all types.SecretArgs FileSources and // EnvSources a Kustomization file in the directory at the provided path refers // to, before walking recursively over all other resources it refers to. // It ignores resource references which refer to absolute or relative paths // outside the working directory of the decryptor, but returns any decryption // error. -func (d *Decryptor) DecryptEnvSources(path string) error { +func (d *Decryptor) DecryptSources(path string) error { if d.kustomization.Spec.Decryption == nil || d.kustomization.Spec.Decryption.Provider != DecryptionProviderSOPS { return nil } decrypted, visited := make(map[string]struct{}, 0), make(map[string]struct{}, 0) - visit := d.decryptKustomizationEnvSources(decrypted) + visit := d.decryptKustomizationSources(decrypted) return recurseKustomizationFiles(d.root, path, visit, visited) } -// decryptKustomizationEnvSources returns a visitKustomization implementation +// decryptKustomizationSources returns a visitKustomization implementation // which attempts to decrypt any EnvSources entry it finds in the Kustomization // file with which it is called. // After decrypting successfully, it adds the absolute path of the file to the // given map. -func (d *Decryptor) decryptKustomizationEnvSources(visited map[string]struct{}) visitKustomization { +func (d *Decryptor) decryptKustomizationSources(visited map[string]struct{}) visitKustomization { return func(root, path string, kus *kustypes.Kustomization) error { visitRef := func(sourcePath string, format formats.Format) error { if !filepath.IsAbs(sourcePath) { @@ -428,19 +500,19 @@ func (d *Decryptor) decryptKustomizationEnvSources(visited map[string]struct{}) if _, ok := visited[absRef]; ok { return nil } - if err := d.sopsDecryptFile(absRef, format, format); err != nil { return securePathErr(root, err) } - // Explicitly set _after_ the decryption operation, this makes // visited work as a list of actually decrypted files visited[absRef] = struct{}{} return nil } + // Iterate over all SecretGenerator entries in the Kustomization file and attempt to decrypt their FileSources and EnvSources. for _, gen := range kus.SecretGenerator { for _, fileSrc := range gen.FileSources { + // Split the source path from any associated key, defaulting to the key if not specified. parts := strings.SplitN(fileSrc, "=", 2) key := parts[0] var filePath string @@ -449,21 +521,36 @@ func (d *Decryptor) decryptKustomizationEnvSources(visited map[string]struct{}) } else { filePath = key } + // Visit the file reference and attempt to decrypt it. if err := visitRef(filePath, formatForPath(key)); err != nil { return err } } for _, envFile := range gen.EnvSources { + // Determine the format for the environment file, defaulting to Dotenv if not specified. format := formatForPath(envFile) if format == formats.Binary { // Default to dotenv format = formats.Dotenv } + // Visit the environment file reference and attempt to decrypt it. if err := visitRef(envFile, format); err != nil { return err } } } + // Iterate over all patches in the Kustomization file and attempt to decrypt their paths if they are encrypted. + for _, patch := range kus.Patches { + if patch.Path == "" { + continue + } + // Determine the format for the patch, defaulting to YAML if not specified. + format := formatForPath(patch.Path) + // Visit the patch reference and attempt to decrypt it. + if err := visitRef(patch.Path, format); err != nil { + return err + } + } return nil } } @@ -503,7 +590,7 @@ func (d *Decryptor) sopsDecryptFile(path string, inputFormat, outputFormat forma if err != nil { return err } - err = os.WriteFile(path, out, 0o644) + err = os.WriteFile(path, out, 0o600) if err != nil { return fmt.Errorf("error writing sops decrypted %s data to %s file: %w", sopsFormatToString[inputFormat], sopsFormatToString[outputFormat], err) @@ -516,7 +603,7 @@ func (d *Decryptor) sopsDecryptFile(path string, inputFormat, outputFormat forma // and then encrypt the file data with the retrieved data key. // It returns the encrypted bytes in the provided output format, or an error. func (d *Decryptor) sopsEncryptWithFormat(metadata sops.Metadata, data []byte, inputFormat, outputFormat formats.Format) ([]byte, error) { - store := common.StoreForFormat(inputFormat) + store := common.StoreForFormat(inputFormat, config.NewStoresConfig()) branches, err := store.LoadPlainFile(data) if err != nil { @@ -543,7 +630,7 @@ func (d *Decryptor) sopsEncryptWithFormat(metadata sops.Metadata, data []byte, i return nil, sopsUserErr("cannot encrypt sops data tree", err) } - outStore := common.StoreForFormat(outputFormat) + outStore := common.StoreForFormat(outputFormat, config.NewStoresConfig()) out, err := outStore.EmitEncryptedFile(tree) if err != nil { return nil, sopsUserErr("failed to emit sops encrypted file", err) @@ -552,29 +639,27 @@ func (d *Decryptor) sopsEncryptWithFormat(metadata sops.Metadata, data []byte, i } // keyServiceServer returns the SOPS (local) key service clients used to serve -// decryption requests. loadKeyServiceServers() is only configured on the first +// decryption requests. loadKeyServiceServer() is only configured on the first // call. func (d *Decryptor) keyServiceServer() []keyservice.KeyServiceClient { d.localServiceOnce.Do(func() { - d.loadKeyServiceServers() + d.loadKeyServiceServer() }) return d.keyServices } -// loadKeyServiceServers loads the SOPS (local) key service clients used to +// loadKeyServiceServer loads the SOPS (local) key service clients used to // serve decryption requests for the current set of Decryptor // credentials. -func (d *Decryptor) loadKeyServiceServers() { +func (d *Decryptor) loadKeyServiceServer() { serverOpts := []intkeyservice.ServerOption{ intkeyservice.WithGnuPGHome(d.gnuPGHome), intkeyservice.WithVaultToken(d.vaultToken), intkeyservice.WithAgeIdentities(d.ageIdentities), - intkeyservice.WithGCPCredsJSON(d.gcpCredsJSON), + intkeyservice.WithAWSCredentialsProvider{CredentialsProvider: d.awsCredentialsProvider}, + intkeyservice.WithAzureTokenCredential{TokenCredential: d.azureTokenCredential}, + intkeyservice.WithGCPTokenSource{TokenSource: d.gcpTokenSource}, } - if d.azureToken != nil { - serverOpts = append(serverOpts, intkeyservice.WithAzureToken{Token: d.azureToken}) - } - serverOpts = append(serverOpts, intkeyservice.WithAWSKeys{CredsProvider: d.awsCredsProvider}) server := intkeyservice.NewServer(serverOpts...) d.keyServices = append(make([]keyservice.KeyServiceClient, 0), keyservice.NewCustomLocalClient(server)) } @@ -699,9 +784,13 @@ func recurseKustomizationFiles(root, path string, visit visitKustomization, visi return err } + // Components may contain resources as well, ... + // ...so we have to process both .resources and .components values + resources := append(kus.Resources, kus.Components...) + // Recurse over other resources in Kustomization, // repeating the above logic per item - for _, res := range kus.Resources { + for _, res := range resources { if !filepath.IsAbs(res) { res = filepath.Join(path, res) } @@ -765,7 +854,7 @@ func stripRoot(root, path string) string { func sopsUserErr(msg string, err error) error { if userErr, ok := err.(sops.UserError); ok { - err = fmt.Errorf(userErr.UserError()) + err = errors.New(userErr.UserError()) } return fmt.Errorf("%s: %w", msg, err) } @@ -794,3 +883,33 @@ func detectFormatFromMarkerBytes(b []byte) formats.Format { } return unsupportedFormat } + +// safeDecrypt redacts secret values in sops error messages. +func safeDecrypt[T any](mac T, err error) (T, error) { + const ( + prefix = "Input string " + suffix = " does not match sops' data format" + ) + + if err == nil { + return mac, nil + } + + var buf strings.Builder + + e := err.Error() + prefIdx := strings.Index(e, prefix) + suffIdx := strings.Index(e, suffix) + + var zero T + if prefIdx == -1 || suffIdx == -1 { + return zero, err + } + + buf.WriteString(e[:prefIdx]) + buf.WriteString(prefix) + buf.WriteString("") + buf.WriteString(suffix) + + return zero, errors.New(buf.String()) +} diff --git a/internal/decryptor/decryptor_test.go b/internal/decryptor/decryptor_test.go index 83fa25964..fde695d3c 100644 --- a/internal/decryptor/decryptor_test.go +++ b/internal/decryptor/decryptor_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/base64" + "errors" "fmt" "io/fs" "os" @@ -30,11 +31,11 @@ import ( "time" extage "filippo.io/age" + "github.com/getsops/sops/v3" + "github.com/getsops/sops/v3/age" + "github.com/getsops/sops/v3/cmd/sops/formats" . "github.com/onsi/gomega" gt "github.com/onsi/gomega/types" - "go.mozilla.org/sops/v3" - sopsage "go.mozilla.org/sops/v3/age" - "go.mozilla.org/sops/v3/cmd/sops/formats" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -45,9 +46,9 @@ import ( kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - "github.com/fluxcd/kustomize-controller/internal/sops/age" "github.com/fluxcd/pkg/apis/meta" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func TestIsEncryptedSecret(t *testing.T) { @@ -209,7 +210,7 @@ aws_session_token: test-token`), }, }, inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { - g.Expect(decryptor.awsCredsProvider).ToNot(BeNil()) + g.Expect(decryptor.awsCredentialsProvider).ToNot(BeNil()) }, }, { @@ -232,7 +233,7 @@ aws_session_token: test-token`), }, }, inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { - g.Expect(decryptor.gcpCredsJSON).ToNot(BeNil()) + g.Expect(decryptor.gcpTokenSource).ToNot(BeNil()) }, }, { @@ -255,7 +256,7 @@ clientSecret: some-client-secret`), }, }, inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { - g.Expect(decryptor.azureToken).ToNot(BeNil()) + g.Expect(decryptor.azureTokenCredential).ToNot(BeNil()) }, }, { @@ -277,7 +278,7 @@ clientSecret: some-client-secret`), }, wantErr: true, inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { - g.Expect(decryptor.azureToken).To(BeNil()) + g.Expect(decryptor.azureTokenCredential).To(BeNil()) }, }, { @@ -299,7 +300,7 @@ clientSecret: some-client-secret`), }, wantErr: true, inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { - g.Expect(decryptor.azureToken).To(BeNil()) + g.Expect(decryptor.azureTokenCredential).To(BeNil()) }, }, { @@ -375,7 +376,7 @@ clientSecret: some-client-secret`), }, } - d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization) + d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization, nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -392,6 +393,60 @@ clientSecret: some-client-secret`), } } +func TestDecryptor_SetAuthOptions(t *testing.T) { + t.Run("nil decryption settings", func(t *testing.T) { + g := NewWithT(t) + + d := &Decryptor{ + kustomization: &kustomizev1.Kustomization{}, + } + + d.SetAuthOptions(context.Background()) + + g.Expect(d.awsCredentialsProvider).To(BeNil()) + g.Expect(d.azureTokenCredential).To(BeNil()) + g.Expect(d.gcpTokenSource).To(BeNil()) + }) + + t.Run("non-sops provider", func(t *testing.T) { + g := NewWithT(t) + + d := &Decryptor{ + kustomization: &kustomizev1.Kustomization{ + Spec: kustomizev1.KustomizationSpec{ + Decryption: &kustomizev1.Decryption{}, + }, + }, + } + + d.SetAuthOptions(context.Background()) + + g.Expect(d.awsCredentialsProvider).To(BeNil()) + g.Expect(d.azureTokenCredential).To(BeNil()) + g.Expect(d.gcpTokenSource).To(BeNil()) + }) + + t.Run("sops provider", func(t *testing.T) { + g := NewWithT(t) + + d := &Decryptor{ + kustomization: &kustomizev1.Kustomization{ + Spec: kustomizev1.KustomizationSpec{ + Decryption: &kustomizev1.Decryption{ + Provider: DecryptionProviderSOPS, + }, + }, + }, + } + + d.SetAuthOptions(context.Background()) + + g.Expect(d.awsCredentialsProvider).NotTo(BeNil()) + g.Expect(d.azureTokenCredential).NotTo(BeNil()) + g.Expect(d.gcpTokenSource).NotTo(BeNil()) + }) +} + func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { t.Run("decrypt INI to INI", func(t *testing.T) { g := NewWithT(t) @@ -405,10 +460,10 @@ func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { } format := formats.Ini - data := []byte("[config]\nkey = value\n\n") + data := []byte("[config]\nkey = value\n") encData, err := kd.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, data, format, format) g.Expect(err).ToNot(HaveOccurred()) @@ -435,7 +490,7 @@ func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { data := []byte("{\"key\": \"value\"}\n") encData, err := kd.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, data, inputFormat, inputFormat) g.Expect(err).ToNot(HaveOccurred()) @@ -467,7 +522,7 @@ func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { format := formats.Binary encData, err := kd.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, []byte("foo bar"), format, format) g.Expect(err).ToNot(HaveOccurred()) @@ -494,7 +549,7 @@ func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { data := []byte("key=value\n") encData, err := kd.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, data, format, format) g.Expect(err).ToNot(HaveOccurred()) @@ -515,11 +570,11 @@ func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { func TestDecryptor_DecryptResource(t *testing.T) { var ( - resourceFactory = provider.NewDefaultDepProvider().GetResourceFactory() - emptyResource = resourceFactory.FromMap(map[string]interface{}{}) + resourceFactory = provider.NewDefaultDepProvider().GetResourceFactory() + emptyResource, _ = resourceFactory.FromMap(map[string]interface{}{}) ) - newSecretResource := func(namespace, name string, data map[string]interface{}) *resource.Resource { + newSecretResource := func(namespace, name string, data map[string]interface{}) (*resource.Resource, error) { return resourceFactory.FromMap(map[string]interface{}{ "apiVersion": "v1", "kind": "Secret", @@ -550,7 +605,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { Provider: DecryptionProviderSOPS, } - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -558,7 +613,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) d.ageIdentities = append(d.ageIdentities, ageID) - secret := newSecretResource("test", "secret", map[string]interface{}{ + secret, _ := newSecretResource("test", "secret", map[string]interface{}{ "key": "value", }) g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse()) @@ -569,7 +624,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { encData, err := d.sopsEncryptWithFormat(sops.Metadata{ EncryptedRegex: "^(data|stringData)$", KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, secretData, formats.Json, formats.Json) g.Expect(err).ToNot(HaveOccurred()) @@ -591,7 +646,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { Provider: DecryptionProviderSOPS, } - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -599,15 +654,15 @@ func TestDecryptor_DecryptResource(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) d.ageIdentities = append(d.ageIdentities, ageID) - plainData := []byte("[config]\napp = secret\n\n") + plainData := []byte("[config]\napp = secret\n") encData, err := d.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, plainData, formats.Ini, formats.Yaml) g.Expect(err).ToNot(HaveOccurred()) - secret := newSecretResource("test", "secret-data", map[string]interface{}{ + secret, _ := newSecretResource("test", "secret-data", map[string]interface{}{ "file.ini": base64.StdEncoding.EncodeToString(encData), }) g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse()) @@ -626,7 +681,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { Provider: DecryptionProviderSOPS, } - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -637,12 +692,12 @@ func TestDecryptor_DecryptResource(t *testing.T) { plainData := []byte("structured:\n data:\n key: value\n") encData, err := d.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, plainData, formats.Yaml, formats.Yaml) g.Expect(err).ToNot(HaveOccurred()) - secret := newSecretResource("test", "secret-data", map[string]interface{}{ + secret, _ := newSecretResource("test", "secret-data", map[string]interface{}{ "key.yaml": base64.StdEncoding.EncodeToString(encData), }) g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse()) @@ -661,7 +716,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { Provider: DecryptionProviderSOPS, } - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -681,12 +736,12 @@ func TestDecryptor_DecryptResource(t *testing.T) { }`) encData, err := d.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + {&age.MasterKey{Recipient: ageID.Recipient().String()}}, }, }, plainData, formats.Json, formats.Yaml) g.Expect(err).ToNot(HaveOccurred()) - secret := resourceFactory.FromMap(map[string]interface{}{ + secret, _ := resourceFactory.FromMap(map[string]interface{}{ "apiVersion": "v1", "kind": "Secret", "metadata": map[string]interface{}{ @@ -703,13 +758,14 @@ func TestDecryptor_DecryptResource(t *testing.T) { got, err := d.DecryptResource(secret) g.Expect(err).ToNot(HaveOccurred()) g.Expect(got).ToNot(BeNil()) - g.Expect(got.GetDataMap()).To(HaveKeyWithValue(corev1.DockerConfigJsonKey, base64.StdEncoding.EncodeToString(plainData))) + plainDataWithTrailingNewline := append(plainData, '\n') // https://github.com/getsops/sops/issues/1825 + g.Expect(got.GetDataMap()).To(HaveKeyWithValue(corev1.DockerConfigJsonKey, base64.StdEncoding.EncodeToString(plainDataWithTrailingNewline))) }) t.Run("nil resource", func(t *testing.T) { g := NewWithT(t) - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy()) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy(), nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -721,7 +777,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { t.Run("no decryption spec", func(t *testing.T) { g := NewWithT(t) - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy()) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy(), nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -737,7 +793,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { kus.Spec.Decryption = &kustomizev1.Decryption{ Provider: "not-supported", } - d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(cleanup) @@ -747,7 +803,7 @@ func TestDecryptor_DecryptResource(t *testing.T) { }) } -func TestDecryptor_decryptKustomizationEnvSources(t *testing.T) { +func TestDecryptor_decryptKustomizationSources(t *testing.T) { type file struct { name string symlink string @@ -900,17 +956,17 @@ func TestDecryptor_decryptKustomizationEnvSources(t *testing.T) { } data, err = d.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: id.Recipient().String()}}, + {&age.MasterKey{Recipient: id.Recipient().String()}}, }, }, f.data, format, format) g.Expect(err).ToNot(HaveOccurred()) g.Expect(data).ToNot(Equal(f.data)) } - g.Expect(os.WriteFile(fPath, data, 0o644)).To(Succeed()) + g.Expect(os.WriteFile(fPath, data, 0o600)).To(Succeed()) } visited := make(map[string]struct{}, 0) - visit := d.decryptKustomizationEnvSources(visited) + visit := d.decryptKustomizationSources(visited) kus := &kustypes.Kustomization{SecretGenerator: tt.secretGenerator} err = visit(root, tt.path, kus) @@ -1042,7 +1098,7 @@ func TestDecryptor_decryptSopsFile(t *testing.T) { if f.encrypt { b, err := d.sopsEncryptWithFormat(sops.Metadata{ KeyGroups: []sops.KeyGroup{ - {&sopsage.MasterKey{Recipient: id.Recipient().String()}}, + {&age.MasterKey{Recipient: id.Recipient().String()}}, }, }, data, f.format, f.format) g.Expect(err).ToNot(HaveOccurred()) @@ -1050,7 +1106,7 @@ func TestDecryptor_decryptSopsFile(t *testing.T) { data = b } g.Expect(os.MkdirAll(filepath.Dir(fPath), 0o700)).To(Succeed()) - g.Expect(os.WriteFile(fPath, data, 0o644)).To(Succeed()) + g.Expect(os.WriteFile(fPath, data, 0o600)).To(Succeed()) } path := filepath.Join(tmpDir, tt.path) @@ -1164,7 +1220,7 @@ func TestDecryptor_secureLoadKustomizationFile(t *testing.T) { continue } g.Expect(os.MkdirAll(filepath.Dir(fPath), 0o700)).To(Succeed()) - g.Expect(os.WriteFile(fPath, f.data, 0o644)).To(Succeed()) + g.Expect(os.WriteFile(fPath, f.data, 0o600)).To(Succeed()) } root := filepath.Join(tmpDir, tt.rootSuffix) @@ -1438,7 +1494,7 @@ func TestDecryptor_recurseKustomizationFiles(t *testing.T) { b, err := yaml.Marshal(kus) g.Expect(err).ToNot(HaveOccurred()) g.Expect(os.MkdirAll(filepath.Dir(path), 0o700)).To(Succeed()) - g.Expect(os.WriteFile(path, b, 0o644)) + g.Expect(os.WriteFile(path, b, 0o600)) } visit := func(root, path string, kus *kustypes.Kustomization) error { @@ -1487,12 +1543,12 @@ func TestDecryptor_isSOPSEncryptedResource(t *testing.T) { g := NewWithT(t) resourceFactory := provider.NewDefaultDepProvider().GetResourceFactory() - encrypted := resourceFactory.FromMap(map[string]interface{}{ + encrypted, _ := resourceFactory.FromMap(map[string]interface{}{ "sops": map[string]string{ "mac": "some mac value", }, }) - empty := resourceFactory.FromMap(map[string]interface{}{}) + empty, _ := resourceFactory.FromMap(map[string]interface{}{}) g.Expect(isSOPSEncryptedResource(encrypted)).To(BeTrue()) g.Expect(isSOPSEncryptedResource(empty)).To(BeFalse()) @@ -1598,3 +1654,54 @@ func TestDecryptor_detectFormatFromMarkerBytes(t *testing.T) { }) } } + +func TestSafeDecrypt(t *testing.T) { + for _, tt := range []struct { + name string + mac string + err string + expectedMac string + expectedErr string + }{ + { + name: "no error", + mac: "some mac", + expectedMac: "some mac", + }, + { + name: "only prefix", + err: "Input string was not in a correct format", + expectedErr: "Input string was not in a correct format", + }, + { + name: "only suffix", + err: "The value does not match sops' data format", + expectedErr: "The value does not match sops' data format", + }, + { + name: "redacted value", + err: "Input string 1234567897 does not match sops' data format", + expectedErr: "Input string does not match sops' data format", + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + var err error + if tt.err != "" { + err = errors.New(tt.err) + } + + mac, err := safeDecrypt(tt.mac, err) + + g.Expect(mac).To(Equal(tt.expectedMac)) + + if tt.expectedErr == "" { + g.Expect(err).To(Not(HaveOccurred())) + } else { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(Equal(tt.expectedErr)) + } + }) + } +} diff --git a/internal/features/features.go b/internal/features/features.go index c0d330efe..b70ce1ccf 100644 --- a/internal/features/features.go +++ b/internal/features/features.go @@ -18,7 +18,10 @@ limitations under the License. // and their default states. package features -import feathelper "github.com/fluxcd/pkg/runtime/features" +import ( + "github.com/fluxcd/pkg/auth" + feathelper "github.com/fluxcd/pkg/runtime/features" +) const ( // CacheSecretsAndConfigMaps controls whether Secrets and ConfigMaps should @@ -27,12 +30,49 @@ const ( // When enabled, it will cache both object types, resulting in increased // memory usage and cluster-wide RBAC permissions (list and watch). CacheSecretsAndConfigMaps = "CacheSecretsAndConfigMaps" + + // DisableStatusPollerCache controls whether the status polling cache + // should be disabled. + // + // This may be useful when the controller is running in a cluster with a + // large number of resources, as it will potentially reduce the amount of + // memory used by the controller. + DisableStatusPollerCache = "DisableStatusPollerCache" + + // DisableFailFastBehavior controls whether the fail-fast behavior when + // waiting for resources to become ready should be disabled. + DisableFailFastBehavior = "DisableFailFastBehavior" + + // StrictPostBuildSubstitutions controls whether the post-build substitutions + // should fail if a variable without a default value is declared in files + // but is missing from the input vars. + StrictPostBuildSubstitutions = "StrictPostBuildSubstitutions" + + // GroupChangelog controls groups kubernetes objects names on log output + // reduces cardinality of logs when logging to elasticsearch + GroupChangeLog = "GroupChangeLog" ) var features = map[string]bool{ // CacheSecretsAndConfigMaps // opt-in from v0.33 CacheSecretsAndConfigMaps: false, + // DisableStatusPollerCache + // opt-out from v1.2 + DisableStatusPollerCache: true, + // DisableFailFastBehavior + // opt-in from v1.1 + DisableFailFastBehavior: false, + // StrictPostBuildSubstitutions + // opt-in from v1.3 + StrictPostBuildSubstitutions: false, + // GroupChangeLog + // opt-in from v1.5 + GroupChangeLog: false, +} + +func init() { + auth.SetFeatureGates(features) } // FeatureGates contains a list of all supported feature gates and diff --git a/internal/inventory/inventory.go b/internal/inventory/inventory.go index 2a01820ec..a92c6946b 100644 --- a/internal/inventory/inventory.go +++ b/internal/inventory/inventory.go @@ -21,12 +21,12 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/cli-utils/pkg/object" + "github.com/fluxcd/cli-utils/pkg/object" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/ssa" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) func New() *kustomizev1.ResourceInventory { diff --git a/internal/inventory/inventory_test.go b/internal/inventory/inventory_test.go index 85861317c..b1450f039 100644 --- a/internal/inventory/inventory_test.go +++ b/internal/inventory/inventory_test.go @@ -21,10 +21,11 @@ import ( "strings" "testing" + "github.com/fluxcd/pkg/ssa" + ssautil "github.com/fluxcd/pkg/ssa/utils" . "github.com/onsi/gomega" - "sigs.k8s.io/cli-utils/pkg/object" - "github.com/fluxcd/pkg/ssa" + "github.com/fluxcd/cli-utils/pkg/object" ) func Test_Inventory(t *testing.T) { @@ -72,7 +73,7 @@ func readManifest(manifest string) (*ssa.ChangeSet, error) { return nil, err } - objects, err := ssa.ReadObjects(strings.NewReader(string(data))) + objects, err := ssautil.ReadObjects(strings.NewReader(string(data))) if err != nil { return nil, err } @@ -83,8 +84,8 @@ func readManifest(manifest string) (*ssa.ChangeSet, error) { cse := ssa.ChangeSetEntry{ ObjMetadata: object.UnstructuredToObjMetadata(o), GroupVersion: o.GroupVersionKind().Version, - Subject: ssa.FmtUnstructured(o), - Action: string(ssa.CreatedAction), + Subject: ssautil.FmtUnstructured(o), + Action: ssa.CreatedAction, } cs.Add(cse) } diff --git a/internal/sops/LICENSE b/internal/sops/LICENSE deleted file mode 100644 index a612ad981..000000000 --- a/internal/sops/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/internal/sops/age/keysource.go b/internal/sops/age/keysource.go deleted file mode 100644 index c63d3686c..000000000 --- a/internal/sops/age/keysource.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (C) 2021 The Mozilla SOPS authors -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package age - -import ( - "bytes" - "fmt" - "io" - "strings" - - "filippo.io/age" - "filippo.io/age/armor" -) - -// MasterKey is an age key used to Encrypt and Decrypt SOPS' data key. -// -// Adapted from https://github.com/mozilla/sops/blob/v3.7.2/age/keysource.go -// to be able to have fine-grain control over the used decryption keys -// without relying on the existence of file(path)s. -type MasterKey struct { - // Identities contains the set of Bench32-encoded age identities used to - // Decrypt. - // They are lazy-loaded using MasterKeyFromIdentities, or on first - // Decrypt(). - // In addition to using this field, ParsedIdentities.ApplyToMasterKey() can - // be used to parse and lazy-load identities. - Identities []string - // Recipient contains the Bench32-encoded age public key used to Encrypt. - Recipient string - // EncryptedKey contains the SOPS data key encrypted with age. - EncryptedKey string - - // parsedIdentities contains a slice of parsed age identities. - // It is used to lazy-load the Identities at-most once. - // It can also be injected by a (local) keyservice.KeyServiceServer using - // ParsedIdentities.ApplyToMasterKey(). - parsedIdentities []age.Identity - // parsedRecipient contains a parsed age public key. - // It is used to lazy-load the Recipient at-most once. - parsedRecipient *age.X25519Recipient -} - -// MasterKeyFromRecipient takes a Bech32-encoded age public key, parses it, and -// returns a new MasterKey. -func MasterKeyFromRecipient(recipient string) (*MasterKey, error) { - parsedRecipient, err := parseRecipient(recipient) - if err != nil { - return nil, err - } - return &MasterKey{ - Recipient: recipient, - parsedRecipient: parsedRecipient, - }, nil -} - -// MasterKeyFromIdentities takes a set if Bech32-encoded age identities, parses -// them, and returns a new MasterKey. -func MasterKeyFromIdentities(identities ...string) (*MasterKey, error) { - parsedIdentities, err := parseIdentities(identities...) - if err != nil { - return nil, err - } - return &MasterKey{ - Identities: identities, - parsedIdentities: parsedIdentities, - }, nil -} - -// ParsedIdentities contains a set of parsed age identities. -// It allows for creating a (local) keyservice.KeyServiceServer which parses -// identities only once, to then inject them using ApplyToMasterKey() for all -// requests. -type ParsedIdentities []age.Identity - -// Import attempts to parse the given identities, to then add them to itself. -// It returns any parsing error. -// A single identity argument is allowed to be a multiline string containing -// multiple identities. Empty lines and lines starting with "#" are ignored. -// It is not thread safe, and parallel importing would better be done by -// parsing (using age.ParseIdentities) and appending to the slice yourself, in -// combination with e.g. a sync.Mutex. -func (i *ParsedIdentities) Import(identity ...string) error { - identities, err := parseIdentities(identity...) - if err != nil { - return fmt.Errorf("failed to parse and add to age identities: %w", err) - } - *i = append(*i, identities...) - return nil -} - -// ApplyToMasterKey configures the ParsedIdentities on the provided key. -func (i ParsedIdentities) ApplyToMasterKey(key *MasterKey) { - key.parsedIdentities = i -} - -// Encrypt takes a SOPS data key, encrypts it with the Recipient, and stores -// the result in the EncryptedKey field. -func (key *MasterKey) Encrypt(dataKey []byte) error { - if key.parsedRecipient == nil { - parsedRecipient, err := parseRecipient(key.Recipient) - if err != nil { - return err - } - key.parsedRecipient = parsedRecipient - } - - var buffer bytes.Buffer - aw := armor.NewWriter(&buffer) - w, err := age.Encrypt(aw, key.parsedRecipient) - if err != nil { - return fmt.Errorf("failed to create writer for encrypting sops data key with age: %w", err) - } - if _, err := w.Write(dataKey); err != nil { - return fmt.Errorf("failed to encrypt sops data key with age: %w", err) - } - if err := w.Close(); err != nil { - return fmt.Errorf("failed to close writer for encrypting sops data key with age: %w", err) - } - if err := aw.Close(); err != nil { - return fmt.Errorf("failed to close armored writer: %w", err) - } - - key.SetEncryptedDataKey(buffer.Bytes()) - return nil -} - -// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been -// encrypted yet. -func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { - if key.EncryptedKey == "" { - return key.Encrypt(dataKey) - } - return nil -} - -// EncryptedDataKey returns the encrypted SOPS data key this master key holds. -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// SetEncryptedDataKey sets the encrypted SOPS data key for this master key. -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -// Decrypt decrypts the EncryptedKey with the (parsed) Identities and returns -// the result. -func (key *MasterKey) Decrypt() ([]byte, error) { - if len(key.parsedIdentities) == 0 && len(key.Identities) > 0 { - parsedIdentities, err := parseIdentities(key.Identities...) - if err != nil { - return nil, err - } - key.parsedIdentities = parsedIdentities - } - - src := bytes.NewReader([]byte(key.EncryptedKey)) - ar := armor.NewReader(src) - r, err := age.Decrypt(ar, key.parsedIdentities...) - if err != nil { - return nil, fmt.Errorf("failed to create reader for decrypting sops data key with age: %w", err) - } - - var b bytes.Buffer - if _, err := io.Copy(&b, r); err != nil { - return nil, fmt.Errorf("failed to copy age decrypted data into bytes.Buffer: %w", err) - } - return b.Bytes(), nil -} - -// NeedsRotation returns whether the data key needs to be rotated or not. -func (key *MasterKey) NeedsRotation() bool { - return false -} - -// ToString converts the key to a string representation. -func (key *MasterKey) ToString() string { - return key.Recipient -} - -// ToMap converts the MasterKey to a map for serialization purposes. -func (key *MasterKey) ToMap() map[string]interface{} { - out := make(map[string]interface{}) - out["recipient"] = key.Recipient - out["enc"] = key.EncryptedKey - return out -} - -// parseRecipient attempts to parse a string containing an encoded age public -// key. -func parseRecipient(recipient string) (*age.X25519Recipient, error) { - parsedRecipient, err := age.ParseX25519Recipient(recipient) - if err != nil { - return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err) - } - return parsedRecipient, nil -} - -// parseIdentities attempts to parse the string set of encoded age identities. -// A single identity argument is allowed to be a multiline string containing -// multiple identities. Empty lines and lines starting with "#" are ignored. -func parseIdentities(identity ...string) ([]age.Identity, error) { - var identities []age.Identity - for _, i := range identity { - parsed, err := age.ParseIdentities(strings.NewReader(i)) - if err != nil { - return nil, err - } - identities = append(identities, parsed...) - } - return identities, nil -} diff --git a/internal/sops/age/keysource_test.go b/internal/sops/age/keysource_test.go deleted file mode 100644 index b32588cb8..000000000 --- a/internal/sops/age/keysource_test.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package age - -import ( - "testing" - - fuzz "github.com/AdaLogics/go-fuzz-headers" - . "github.com/onsi/gomega" - "go.mozilla.org/sops/v3/age" -) - -const ( - mockRecipient string = "age1lzd99uklcjnc0e7d860axevet2cz99ce9pq6tzuzd05l5nr28ams36nvun" - mockIdentity string = "AGE-SECRET-KEY-1G0Q5K9TV4REQ3ZSQRMTMG8NSWQGYT0T7TZ33RAZEE0GZYVZN0APSU24RK7" - - // mockUnrelatedIdentity is not actually utilized in tests, but confirms we - // iterate over all available identities. - mockUnrelatedIdentity string = "AGE-SECRET-KEY-1432K5YRNSC44GC4986NXMX6GVZ52WTMT9C79CLUVWYY4DKDHD5JSNDP4MC" - - // mockEncryptedKey equals to "data" when decrypted with mockIdentity. - mockEncryptedKey string = `-----BEGIN AGE ENCRYPTED FILE----- -YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY2t2NkdLUGRvY3l2OGNy -MVJWcUhCOEZrUG8yeCtnRnhxL0I5NFk4YjJFCmE4SVQ3MEdyZkFqRWpSa2F0NVhF -VDUybzBxdS9nSGpHSVRVMUI0UEVqZkkKLS0tIGJjeGhNQ0Y5L2VZRVVYSm90djFF -bzdnQ3UwTGljMmtrbWNMV1MxYkFzUFUK4xjOZOTGdcbzuwUY/zeBXhcF+Md3e5PQ -EylloI7MNGbadPGb ------END AGE ENCRYPTED FILE-----` -) - -var ( - mockIdentities = []string{ - mockUnrelatedIdentity, - mockIdentity, - } -) - -func TestMasterKeyFromRecipient(t *testing.T) { - g := NewWithT(t) - - got, err := MasterKeyFromRecipient(mockRecipient) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(got).ToNot(BeNil()) - g.Expect(got.Recipient).To(Equal(mockRecipient)) - g.Expect(got.parsedRecipient).ToNot(BeNil()) - g.Expect(got.parsedIdentities).To(BeEmpty()) - - got, err = MasterKeyFromRecipient("invalid") - g.Expect(err).To(HaveOccurred()) - g.Expect(got).To(BeNil()) -} - -func TestMasterKeyFromIdentities(t *testing.T) { - g := NewWithT(t) - - got, err := MasterKeyFromIdentities(mockIdentities...) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(got).ToNot(BeNil()) - g.Expect(got.Identities).To(HaveLen(2)) - g.Expect(got.Identities).To(ContainElements(mockIdentities)) - g.Expect(got.parsedIdentities).To(HaveLen(2)) - - got, err = MasterKeyFromIdentities("invalid") - g.Expect(err).To(HaveOccurred()) - g.Expect(got).To(BeNil()) -} - -func TestParsedIdentities_Import(t *testing.T) { - g := NewWithT(t) - - i := make(ParsedIdentities, 0) - g.Expect(i.Import(mockIdentities...)).To(Succeed()) - g.Expect(i).To(HaveLen(2)) - - g.Expect(i.Import("invalid")).To(HaveOccurred()) - g.Expect(i).To(HaveLen(2)) -} - -func TestParsedIdentities_ApplyToMasterKey(t *testing.T) { - g := NewWithT(t) - - i := make(ParsedIdentities, 0) - g.Expect(i.Import(mockIdentities...)).To(Succeed()) - - key := &MasterKey{} - i.ApplyToMasterKey(key) - g.Expect(key.parsedIdentities).To(BeEquivalentTo(i)) -} - -func TestMasterKey_Encrypt(t *testing.T) { - mockParsedRecipient, _ := parseRecipient(mockRecipient) - - tests := []struct { - name string - key *MasterKey - wantErr bool - }{ - { - name: "recipient", - key: &MasterKey{ - Recipient: mockRecipient, - }, - }, - { - name: "parsed recipient", - key: &MasterKey{ - parsedRecipient: mockParsedRecipient, - }, - }, - { - name: "invalid recipient", - key: &MasterKey{ - Recipient: "invalid", - }, - wantErr: true, - }, - { - name: "parsed recipient and invalid recipient", - key: &MasterKey{ - Recipient: "invalid", - parsedRecipient: mockParsedRecipient, - }, - // This should pass, confirming parsedRecipient > Recipient - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - err := tt.key.Encrypt([]byte("data")) - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - g.Expect(tt.key.EncryptedKey).To(BeEmpty()) - return - } - - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(tt.key.EncryptedKey).ToNot(BeEmpty()) - g.Expect(tt.key.EncryptedKey).To(ContainSubstring("AGE ENCRYPTED FILE")) - }) - } -} - -func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - dataKey := []byte("foo") - - encryptKey := &MasterKey{ - Recipient: mockRecipient, - } - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - t.Setenv(age.SopsAgeKeyEnv, mockIdentity) - decryptKey := &age.MasterKey{ - EncryptedKey: encryptKey.EncryptedKey, - } - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptIfNeeded(t *testing.T) { - g := NewWithT(t) - - key, err := MasterKeyFromRecipient(mockRecipient) - g.Expect(err).ToNot(HaveOccurred()) - - g.Expect(key.EncryptIfNeeded([]byte("data"))).To(Succeed()) - - encryptedKey := key.EncryptedKey - g.Expect(encryptedKey).To(ContainSubstring("AGE ENCRYPTED FILE")) - - g.Expect(key.EncryptIfNeeded([]byte("some other data"))).To(Succeed()) - g.Expect(key.EncryptedKey).To(Equal(encryptedKey)) -} - -func TestMasterKey_EncryptedDataKey(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{EncryptedKey: "some key"} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) -} - -func TestMasterKey_Decrypt(t *testing.T) { - parsedIdentities, _ := parseIdentities(mockIdentities...) - - tests := []struct { - name string - key *MasterKey - wantErr bool - }{ - { - name: "identities", - key: &MasterKey{ - Identities: mockIdentities, - EncryptedKey: mockEncryptedKey, - }, - }, - { - name: "parsed identities", - key: &MasterKey{ - EncryptedKey: mockEncryptedKey, - parsedIdentities: parsedIdentities, - }, - }, - { - name: "no identities", - key: &MasterKey{ - Identities: []string{}, - EncryptedKey: mockEncryptedKey, - }, - wantErr: true, - }, - { - name: "invalid identity", - key: &MasterKey{ - Identities: []string{"invalid"}, - EncryptedKey: mockEncryptedKey, - }, - wantErr: true, - }, - { - name: "invalid encrypted key", - key: &MasterKey{ - Identities: mockIdentities, - EncryptedKey: "invalid", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - data, err := tt.key.Decrypt() - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - g.Expect(data).To(BeNil()) - return - } - - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(data).To(Equal([]byte("data"))) - }) - } -} - -func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - dataKey := []byte("foo") - - encryptKey := &age.MasterKey{ - Recipient: mockRecipient, - } - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - decryptKey := &MasterKey{ - Identities: []string{mockIdentity}, - EncryptedKey: encryptKey.EncryptedKey, - } - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { - g := NewWithT(t) - - encryptKey, err := MasterKeyFromRecipient(mockRecipient) - g.Expect(err).ToNot(HaveOccurred()) - - data := []byte("some secret data") - g.Expect(encryptKey.Encrypt(data)).To(Succeed()) - g.Expect(encryptKey.EncryptedKey).ToNot(BeEmpty()) - - decryptKey, err := MasterKeyFromIdentities(mockIdentity) - g.Expect(err).ToNot(HaveOccurred()) - - decryptKey.EncryptedKey = encryptKey.EncryptedKey - decryptedData, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decryptedData).To(Equal(data)) -} - -func TestMasterKey_ToString(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{Recipient: mockRecipient} - g.Expect(key.ToString()).To(Equal(key.Recipient)) -} - -func TestMasterKey_ToMap(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{ - Recipient: mockRecipient, - Identities: mockIdentities, // must never be included - EncryptedKey: "some-encrypted-key", - } - g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ - "recipient": mockRecipient, - "enc": key.EncryptedKey, - })) -} - -func Fuzz_Age(f *testing.F) { - f.Fuzz(func(t *testing.T, receipt, identities string, seed, data []byte) { - fc := fuzz.NewConsumer(seed) - masterKey := MasterKey{} - - if err := fc.GenerateStruct(&masterKey); err != nil { - return - } - - _ = masterKey.Encrypt(data) - _ = masterKey.EncryptIfNeeded(data) - _, _ = MasterKeyFromRecipient(receipt) - _, _ = MasterKeyFromIdentities(identities) - }) -} diff --git a/internal/sops/awskms/credentials.go b/internal/sops/awskms/credentials.go new file mode 100644 index 000000000..d95bd5af1 --- /dev/null +++ b/internal/sops/awskms/credentials.go @@ -0,0 +1,39 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awskms + +import ( + "fmt" + + "github.com/aws/aws-sdk-go-v2/credentials" + "sigs.k8s.io/yaml" +) + +// LoadStaticCredentialsFromYAML parses the given YAML and returns a +// credentials.StaticCredentialsProvider that can be used to authenticate with +// AWS, or an error if the YAML could not be parsed. +func LoadStaticCredentialsFromYAML(b []byte) (credentials.StaticCredentialsProvider, error) { + d := struct { + AccessKeyID string `json:"aws_access_key_id"` + SecretAccessKey string `json:"aws_secret_access_key"` + SessionToken string `json:"aws_session_token"` + }{} + if err := yaml.Unmarshal(b, &d); err != nil { + return credentials.StaticCredentialsProvider{}, fmt.Errorf("failed to unmarshal AWS credentials file: %w", err) + } + return credentials.NewStaticCredentialsProvider(d.AccessKeyID, d.SecretAccessKey, d.SessionToken), nil +} diff --git a/internal/sops/awskms/credentials_test.go b/internal/sops/awskms/credentials_test.go new file mode 100644 index 000000000..6f5d1cc9f --- /dev/null +++ b/internal/sops/awskms/credentials_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awskms + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" +) + +func TestLoadStaticCredentialsFromYAML(t *testing.T) { + g := NewWithT(t) + credsYaml := []byte(` +aws_access_key_id: test-id +aws_secret_access_key: test-secret +aws_session_token: test-token +`) + credsProvider, err := LoadStaticCredentialsFromYAML(credsYaml) + g.Expect(err).ToNot(HaveOccurred()) + + creds, err := credsProvider.Retrieve(context.TODO()) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(creds.AccessKeyID).To(Equal("test-id")) + g.Expect(creds.SecretAccessKey).To(Equal("test-secret")) + g.Expect(creds.SessionToken).To(Equal("test-token")) +} diff --git a/internal/sops/awskms/keysource.go b/internal/sops/awskms/keysource.go deleted file mode 100644 index c287ad683..000000000 --- a/internal/sops/awskms/keysource.go +++ /dev/null @@ -1,291 +0,0 @@ -/* -Copyright (C) 2022 The Flux authors - -This Source Code Form is subject to the terms of the Mozilla Public -License, v. 2.0. If a copy of the MPL was not distributed with this -file, You can obtain one at https://mozilla.org/MPL/2.0/. -*/ - -package awskms - -import ( - "context" - "fmt" - "os" - "regexp" - "strings" - "time" - - "encoding/base64" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/aws/aws-sdk-go-v2/service/sts" - "sigs.k8s.io/yaml" -) - -const ( - // arnRegex matches an AWS ARN. - // valid ARN example: arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48 - arnRegex = `^arn:aws[\w-]*:kms:(.+):[0-9]+:(key|alias)/.+$` - // stsSessionRegex matches an AWS STS session name. - // valid STS session examples: john_s, sops@42WQm042 - stsSessionRegex = "[^a-zA-Z0-9=,.@-_]+" - // kmsTTL is the duration after which a MasterKey requires rotation. - kmsTTL = time.Hour * 24 * 30 * 6 - // roleSessionNameLengthLimit is the AWS role session name length limit. - roleSessionNameLengthLimit = 64 -) - -// MasterKey is an AWS KMS key used to encrypt and decrypt sops' data key. -// Adapted from: https://github.com/mozilla/sops/blob/v3.7.2/kms/keysource.go#L39 -// Modified to accept custom static credentials as opposed to using env vars by default -// and use aws-sdk-go-v2 instead of aws-sdk-go being used in upstream. -type MasterKey struct { - // AWS Role ARN associated with the KMS key. - Arn string - // AWS Role ARN used to assume a role through AWS STS. - Role string - // EncryptedKey stores the data key in it's encrypted form. - EncryptedKey string - // CreationDate is when this MasterKey was created. - CreationDate time.Time - // EncryptionContext provides additional context about the data key. - // Ref: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context - EncryptionContext map[string]string - // AWSProfile is the profile to use for loading configuration and credentials. - AwsProfile string - - // credentialsProvider is used to configure the AWS config with the - // necessary credentials. - credentialsProvider aws.CredentialsProvider - - // epResolver can be used to override the endpoint the AWS client resolves - // to by default. This is mostly used for testing purposes as it can not be - // injected using e.g. an environment variable. The field is not publicly - // exposed, nor configurable. - epResolver aws.EndpointResolverWithOptions -} - -// CredsProvider is a wrapper around aws.CredentialsProvider used for authenticating -// towards AWS KMS. -type CredsProvider struct { - credsProvider aws.CredentialsProvider -} - -// NewCredsProvider returns a CredsProvider object with the provided aws.CredentialsProvider. -func NewCredsProvider(cp aws.CredentialsProvider) *CredsProvider { - return &CredsProvider{ - credsProvider: cp, - } -} - -// ApplyToMasterKey configures the credentials the provided key. -func (c CredsProvider) ApplyToMasterKey(key *MasterKey) { - key.credentialsProvider = c.credsProvider -} - -// LoadCredsProviderFromYaml parses the given YAML returns a CredsProvider object -// which contains the credentials provider used for authenticating towards AWS KMS. -func LoadCredsProviderFromYaml(b []byte) (*CredsProvider, error) { - credInfo := struct { - AccessKeyID string `json:"aws_access_key_id"` - SecretAccessKey string `json:"aws_secret_access_key"` - SessionToken string `json:"aws_session_token"` - }{} - if err := yaml.Unmarshal(b, &credInfo); err != nil { - return nil, fmt.Errorf("failed to unmarshal AWS credentials file: %w", err) - } - return &CredsProvider{ - credsProvider: credentials.NewStaticCredentialsProvider(credInfo.AccessKeyID, - credInfo.SecretAccessKey, credInfo.SessionToken), - }, nil -} - -// EncryptedDataKey returns the encrypted data key this master key holds. -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// SetEncryptedDataKey sets the encrypted data key for this master key. -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -// Encrypt takes a SOPS data key, encrypts it with KMS and stores the result -// in the EncryptedKey field. -func (key *MasterKey) Encrypt(dataKey []byte) error { - cfg, err := key.createKMSConfig() - if err != nil { - return err - } - client := kms.NewFromConfig(*cfg) - input := &kms.EncryptInput{ - KeyId: &key.Arn, - Plaintext: dataKey, - EncryptionContext: key.EncryptionContext, - } - out, err := client.Encrypt(context.TODO(), input) - if err != nil { - return fmt.Errorf("failed to encrypt sops data key with AWS KMS: %w", err) - } - key.EncryptedKey = base64.StdEncoding.EncodeToString(out.CiphertextBlob) - return nil -} - -// EncryptIfNeeded encrypts the provided sops' data key and encrypts it, if it -// has not been encrypted yet. -func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { - if key.EncryptedKey == "" { - return key.Encrypt(dataKey) - } - return nil -} - -// Decrypt decrypts the EncryptedKey field with AWS KMS and returns the result. -func (key *MasterKey) Decrypt() ([]byte, error) { - k, err := base64.StdEncoding.DecodeString(key.EncryptedKey) - if err != nil { - return nil, fmt.Errorf("error base64-decoding encrypted data key: %s", err) - } - cfg, err := key.createKMSConfig() - if err != nil { - return nil, err - } - client := kms.NewFromConfig(*cfg) - input := &kms.DecryptInput{ - KeyId: &key.Arn, - CiphertextBlob: k, - EncryptionContext: key.EncryptionContext, - } - decrypted, err := client.Decrypt(context.TODO(), input) - if err != nil { - return nil, fmt.Errorf("failed to decrypt sops data key with AWS KMS: %w", err) - } - return decrypted.Plaintext, nil -} - -// NeedsRotation returns whether the data key needs to be rotated or not. -func (key *MasterKey) NeedsRotation() bool { - return time.Since(key.CreationDate) > kmsTTL -} - -// ToString converts the key to a string representation. -func (key *MasterKey) ToString() string { - return key.Arn -} - -// ToMap converts the MasterKey to a map for serialization purposes. -func (key MasterKey) ToMap() map[string]interface{} { - out := make(map[string]interface{}) - out["arn"] = key.Arn - if key.Role != "" { - out["role"] = key.Role - } - out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) - out["enc"] = key.EncryptedKey - if key.EncryptionContext != nil { - outcontext := make(map[string]string) - for k, v := range key.EncryptionContext { - outcontext[k] = v - } - out["context"] = outcontext - } - return out -} - -// NewMasterKey creates a new MasterKey from an ARN, role and context, setting the -// creation date to the current date. -func NewMasterKey(arn string, role string, context map[string]string) *MasterKey { - return &MasterKey{ - Arn: arn, - Role: role, - EncryptionContext: context, - CreationDate: time.Now().UTC(), - } -} - -// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that -// ARN. -func NewMasterKeyFromArn(arn string, context map[string]string, awsProfile string) *MasterKey { - k := &MasterKey{} - arn = strings.Replace(arn, " ", "", -1) - roleIndex := strings.Index(arn, "+arn:aws:iam::") - if roleIndex > 0 { - k.Arn = arn[:roleIndex] - k.Role = arn[roleIndex+1:] - } else { - k.Arn = arn - } - k.EncryptionContext = context - k.CreationDate = time.Now().UTC() - k.AwsProfile = awsProfile - return k -} - -// createKMSConfig returns a Config configured with the appropriate credentials. -func (key MasterKey) createKMSConfig() (*aws.Config, error) { - re := regexp.MustCompile(arnRegex) - matches := re.FindStringSubmatch(key.Arn) - if matches == nil { - return nil, fmt.Errorf("no valid ARN found in '%s'", key.Arn) - } - region := matches[1] - cfg, err := config.LoadDefaultConfig(context.TODO(), func(lo *config.LoadOptions) error { - // Use the credentialsProvider if present, otherwise default to reading credentials - // from the environment. - if key.credentialsProvider != nil { - lo.Credentials = key.credentialsProvider - } - if key.AwsProfile != "" { - lo.SharedConfigProfile = key.AwsProfile - } - lo.Region = region - - // Set the epResolver, if present. Used ONLY for tests. - if key.epResolver != nil { - lo.EndpointResolverWithOptions = key.epResolver - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("couldn't load AWS config: %w", err) - } - if key.Role != "" { - return key.createSTSConfig(&cfg) - } - - return &cfg, nil -} - -// createSTSConfig uses AWS STS to assume a role and returns a Config configured -// with that role's credentials. -func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { - hostname, err := os.Hostname() - if err != nil { - return nil, err - } - stsRoleSessionNameRe := regexp.MustCompile(stsSessionRegex) - - sanitizedHostname := stsRoleSessionNameRe.ReplaceAllString(hostname, "") - name := "sops@" + sanitizedHostname - if len(name) >= roleSessionNameLengthLimit { - name = name[:roleSessionNameLengthLimit] - } - - client := sts.NewFromConfig(*config) - input := &sts.AssumeRoleInput{ - RoleArn: &key.Role, - RoleSessionName: &name, - } - out, err := client.AssumeRole(context.TODO(), input) - if err != nil { - return nil, fmt.Errorf("failed to assume role '%s': %w", key.Role, err) - } - config.Credentials = credentials.NewStaticCredentialsProvider(*out.Credentials.AccessKeyId, - *out.Credentials.SecretAccessKey, *out.Credentials.SessionToken, - ) - return config, nil -} diff --git a/internal/sops/awskms/keysource_test.go b/internal/sops/awskms/keysource_test.go deleted file mode 100644 index 54f5040d1..000000000 --- a/internal/sops/awskms/keysource_test.go +++ /dev/null @@ -1,406 +0,0 @@ -/* -Copyright (C) 2022 The Flux authors - -This Source Code Form is subject to the terms of the Mozilla Public -License, v. 2.0. If a copy of the MPL was not distributed with this -file, You can obtain one at https://mozilla.org/MPL/2.0/. -*/ - -package awskms - -import ( - "context" - "encoding/base64" - "fmt" - logger "log" - "os" - "testing" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/kms" - awsv1 "github.com/aws/aws-sdk-go/aws" - sessionv1 "github.com/aws/aws-sdk-go/aws/session" - kmsv1 "github.com/aws/aws-sdk-go/service/kms" - . "github.com/onsi/gomega" - "github.com/ory/dockertest/v3" -) - -var ( - testKMSServerURL string - testKMSARN string -) - -const ( - dummyARN = "arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48" - testLocalKMSTag = "3.11.1" - testLocalKMSImage = "nsmithuk/local-kms" -) - -// TestMain initializes a AWS KMS server using Docker, writes the HTTP address to -// testAWSEndpoint, tries to generate a key for encryption-decryption using a -// backoff retry approach and then sets testKMSARN to the id of the generated key. -// It then runs all the tests, which can make use of the various `test*` variables. -func TestMain(m *testing.M) { - // Uses a sensible default on Windows (TCP/HTTP) and Linux/MacOS (socket) - pool, err := dockertest.NewPool("") - if err != nil { - logger.Fatalf("could not connect to docker: %s", err) - } - - // Pull the image, create a container based on it, and run it - // resource, err := pool.Run("nsmithuk/local-kms", testLocalKMSVersion, []string{}) - resource, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: testLocalKMSImage, - Tag: testLocalKMSTag, - ExposedPorts: []string{"8080"}, - }) - if err != nil { - logger.Fatalf("could not start resource: %s", err) - } - - purgeResource := func() { - if err := pool.Purge(resource); err != nil { - logger.Printf("could not purge resource: %s", err) - } - } - - testKMSServerURL = fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8080/tcp")) - masterKey := createTestMasterKey(dummyARN) - - kmsClient, err := createTestKMSClient(masterKey) - if err != nil { - purgeResource() - logger.Fatalf("could not create session: %s", err) - } - - var key *kms.CreateKeyOutput - if err := pool.Retry(func() error { - key, err = kmsClient.CreateKey(context.TODO(), &kms.CreateKeyInput{}) - if err != nil { - return err - } - return nil - }); err != nil { - purgeResource() - logger.Fatalf("could not create key: %s", err) - } - - if key.KeyMetadata.Arn != nil { - testKMSARN = *key.KeyMetadata.Arn - } else { - purgeResource() - logger.Fatalf("could not set arn") - } - - // Run the tests, but only if we succeeded in setting up the AWS KMS server. - var code int - if err == nil { - code = m.Run() - } - - // This can't be deferred, as os.Exit simpy does not care - if err := pool.Purge(resource); err != nil { - logger.Fatalf("could not purge resource: %s", err) - } - - os.Exit(code) -} - -func TestMasterKey_Encrypt(t *testing.T) { - g := NewWithT(t) - key := createTestMasterKey(testKMSARN) - dataKey := []byte("thisistheway") - g.Expect(key.Encrypt(dataKey)).To(Succeed()) - g.Expect(key.EncryptedKey).ToNot(BeEmpty()) - - kmsClient, err := createTestKMSClient(key) - g.Expect(err).ToNot(HaveOccurred()) - - k, err := base64.StdEncoding.DecodeString(key.EncryptedKey) - g.Expect(err).ToNot(HaveOccurred()) - - input := &kms.DecryptInput{ - CiphertextBlob: k, - EncryptionContext: key.EncryptionContext, - } - decrypted, err := kmsClient.Decrypt(context.TODO(), input) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decrypted.Plaintext).To(Equal(dataKey)) -} - -func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - encryptKey := createTestMasterKey(testKMSARN) - dataKey := []byte("encrypt-compat") - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - // This is the core decryption logic of `sopskms.MasterKey.Decrypt()`. - // We don't call `sops.MasterKey.Decrypt()` directly to avoid issues with - // session and config setup. - config := awsv1.Config{ - Region: awsv1.String("us-west-2"), - Endpoint: &testKMSServerURL, - } - t.Setenv("AWS_ACCESS_KEY_ID", "id") - t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") - k, err := base64.StdEncoding.DecodeString(encryptKey.EncryptedKey) - g.Expect(err).ToNot(HaveOccurred()) - sess, err := sessionv1.NewSessionWithOptions(sessionv1.Options{ - Config: config, - }) - kmsSvc := kmsv1.New(sess) - decrypted, err := kmsSvc.Decrypt(&kmsv1.DecryptInput{CiphertextBlob: k}) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decrypted.Plaintext).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptIfNeeded(t *testing.T) { - g := NewWithT(t) - - key := createTestMasterKey(testKMSARN) - g.Expect(key.EncryptIfNeeded([]byte("data"))).To(Succeed()) - - encryptedKey := key.EncryptedKey - g.Expect(encryptedKey).ToNot(BeEmpty()) - - g.Expect(key.EncryptIfNeeded([]byte("some other data"))).To(Succeed()) - g.Expect(key.EncryptedKey).To(Equal(encryptedKey)) -} - -func TestMasterKey_EncryptedDataKey(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{EncryptedKey: "some key"} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) -} - -func TestMasterKey_Decrypt(t *testing.T) { - g := NewWithT(t) - - key := createTestMasterKey(testKMSARN) - kmsClient, err := createTestKMSClient(key) - g.Expect(err).ToNot(HaveOccurred()) - - dataKey := []byte("itsalwaysdns") - out, err := kmsClient.Encrypt(context.TODO(), &kms.EncryptInput{ - Plaintext: dataKey, KeyId: &key.Arn, EncryptionContext: key.EncryptionContext, - }) - g.Expect(err).ToNot(HaveOccurred()) - - key.EncryptedKey = base64.StdEncoding.EncodeToString(out.CiphertextBlob) - got, err := key.Decrypt() - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(got).To(Equal(dataKey)) -} - -func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - // This is the core encryption logic of `sopskms.MasterKey.Encrypt()`. - // We don't call `sops.MasterKey.Encrypt()` directly to avoid issues with - // session and config setup. - dataKey := []byte("decrypt-compat") - config := awsv1.Config{ - Region: awsv1.String("us-west-2"), - Endpoint: &testKMSServerURL, - } - t.Setenv("AWS_ACCESS_KEY_ID", "id") - t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") - sess, err := sessionv1.NewSessionWithOptions(sessionv1.Options{ - Config: config, - }) - kmsSvc := kmsv1.New(sess) - encrypted, err := kmsSvc.Encrypt(&kmsv1.EncryptInput{Plaintext: dataKey, KeyId: &testKMSARN}) - g.Expect(err).ToNot(HaveOccurred()) - - decryptKey := createTestMasterKey(testKMSARN) - decryptKey.EncryptedKey = base64.StdEncoding.EncodeToString(encrypted.CiphertextBlob) - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { - g := NewWithT(t) - - dataKey := []byte("thisistheway") - - encryptKey := createTestMasterKey(testKMSARN) - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - g.Expect(encryptKey.EncryptedKey).ToNot(BeEmpty()) - - decryptKey := createTestMasterKey(testKMSARN) - decryptKey.EncryptedKey = encryptKey.EncryptedKey - - decryptedData, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decryptedData).To(Equal(dataKey)) -} - -func TestMasterKey_NeedsRotation(t *testing.T) { - g := NewWithT(t) - - key := NewMasterKeyFromArn(dummyARN, nil, "") - g.Expect(key.NeedsRotation()).To(BeFalse()) - - key.CreationDate = key.CreationDate.Add(-(kmsTTL + time.Second)) - g.Expect(key.NeedsRotation()).To(BeTrue()) -} - -func TestMasterKey_ToMap(t *testing.T) { - g := NewWithT(t) - key := MasterKey{ - Arn: "test-arn", - Role: "test-role", - EncryptedKey: "enc-key", - EncryptionContext: map[string]string{ - "env": "test", - }, - } - g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ - "arn": "test-arn", - "role": "test-role", - "created_at": "0001-01-01T00:00:00Z", - "enc": "enc-key", - "context": map[string]string{ - "env": "test", - }, - })) -} - -func TestCreds_ApplyToMasterKey(t *testing.T) { - g := NewWithT(t) - - creds := CredsProvider{ - credsProvider: credentials.NewStaticCredentialsProvider("", "", ""), - } - key := &MasterKey{} - creds.ApplyToMasterKey(key) - g.Expect(key.credentialsProvider).To(Equal(creds.credsProvider)) -} - -func TestLoadAwsKmsCredsFromYaml(t *testing.T) { - g := NewWithT(t) - credsYaml := []byte(` -aws_access_key_id: test-id -aws_secret_access_key: test-secret -aws_session_token: test-token -`) - credsProvider, err := LoadCredsProviderFromYaml(credsYaml) - g.Expect(err).ToNot(HaveOccurred()) - - creds, err := credsProvider.credsProvider.Retrieve(context.TODO()) - g.Expect(err).ToNot(HaveOccurred()) - - g.Expect(creds.AccessKeyID).To(Equal("test-id")) - g.Expect(creds.SecretAccessKey).To(Equal("test-secret")) - g.Expect(creds.SessionToken).To(Equal("test-token")) -} - -func Test_createKMSConfig(t *testing.T) { - tests := []struct { - name string - key MasterKey - assertFunc func(g *WithT, cfg *aws.Config, err error) - fallback bool - }{ - { - name: "master key with invalid arn fails", - key: MasterKey{ - Arn: "arn:gcp:kms:antartica-north-2::key/45e6-aca6-a5b005693a48", - }, - assertFunc: func(g *WithT, _ *aws.Config, err error) { - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("no valid ARN found")) - }, - }, - { - name: "master key with with proper configuration passes", - key: MasterKey{ - credentialsProvider: credentials.NewStaticCredentialsProvider("test-id", "test-secret", "test-token"), - AwsProfile: "test-profile", - Arn: "arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", - }, - assertFunc: func(g *WithT, cfg *aws.Config, err error) { - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cfg.Region).To(Equal("us-west-2")) - - creds, err := cfg.Credentials.Retrieve(context.TODO()) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(creds.AccessKeyID).To(Equal("test-id")) - g.Expect(creds.SecretAccessKey).To(Equal("test-secret")) - g.Expect(creds.SessionToken).To(Equal("test-token")) - - // ConfigSources is a slice of config.Config, which in turn is an interface. - // Since we use a LoadOptions object, we assert the type of cfgSrc and then - // check if the expected profile is present. - for _, cfgSrc := range cfg.ConfigSources { - if src, ok := cfgSrc.(config.LoadOptions); ok { - g.Expect(src.SharedConfigProfile).To(Equal("test-profile")) - } - } - }, - }, - { - name: "master key without creds and profile falls back to the environment variables", - key: MasterKey{ - Arn: "arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", - }, - fallback: true, - assertFunc: func(g *WithT, cfg *aws.Config, err error) { - g.Expect(err).ToNot(HaveOccurred()) - - creds, err := cfg.Credentials.Retrieve(context.TODO()) - g.Expect(creds.AccessKeyID).To(Equal("id")) - g.Expect(creds.SecretAccessKey).To(Equal("secret")) - g.Expect(creds.SessionToken).To(Equal("token")) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - // Set the environment variables if we want to fallback - if tt.fallback { - t.Setenv("AWS_ACCESS_KEY_ID", "id") - t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") - t.Setenv("AWS_SESSION_TOKEN", "token") - } - cfg, err := tt.key.createKMSConfig() - tt.assertFunc(g, cfg, err) - }) - } -} - -func createTestMasterKey(arn string) MasterKey { - return MasterKey{ - Arn: arn, - credentialsProvider: credentials.NewStaticCredentialsProvider("id", "secret", ""), - epResolver: epResolver{}, - } -} - -// epResolver is a dummy resolver that points to the local test KMS server -type epResolver struct{} - -func (e epResolver) ResolveEndpoint(service, region string, options ...interface{}) (aws.Endpoint, error) { - return aws.Endpoint{ - URL: testKMSServerURL, - }, nil -} - -func createTestKMSClient(key MasterKey) (*kms.Client, error) { - cfg, err := key.createKMSConfig() - if err != nil { - return nil, err - } - - cfg.EndpointResolverWithOptions = epResolver{} - - return kms.NewFromConfig(*cfg), nil -} diff --git a/internal/sops/awskms/region.go b/internal/sops/awskms/region.go new file mode 100644 index 000000000..686b39bb3 --- /dev/null +++ b/internal/sops/awskms/region.go @@ -0,0 +1,27 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awskms + +import ( + "strings" +) + +// GetRegionFromKMSARN extracts the region from a KMS ARN. +func GetRegionFromKMSARN(arn string) string { + arn = strings.TrimPrefix(arn, "arn:aws:kms:") + return strings.SplitN(arn, ":", 2)[0] +} diff --git a/internal/sops/awskms/region_test.go b/internal/sops/awskms/region_test.go new file mode 100644 index 000000000..98a587d67 --- /dev/null +++ b/internal/sops/awskms/region_test.go @@ -0,0 +1,34 @@ +/* +Copyright 2025 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awskms_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/fluxcd/kustomize-controller/internal/sops/awskms" +) + +func TestGetRegionFromKMSARN(t *testing.T) { + g := NewWithT(t) + + arn := "arn:aws:kms:us-east-1:211125720409:key/mrk-3179bb7e88bc42ffb1a27d5038ceea25" + + region := awskms.GetRegionFromKMSARN(arn) + g.Expect(region).To(Equal("us-east-1")) +} diff --git a/internal/sops/azkv/config.go b/internal/sops/azkv/config.go index 4685cddac..2de7fd889 100644 --- a/internal/sops/azkv/config.go +++ b/internal/sops/azkv/config.go @@ -1,17 +1,32 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package azkv import ( + "bytes" + "encoding/binary" "fmt" + "io" + "unicode/utf16" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/dimchansky/utfbom" "sigs.k8s.io/yaml" ) @@ -50,7 +65,7 @@ type AZConfig struct { Password string `json:"password,omitempty"` } -// TokenFromAADConfig attempts to construct a Token using the AADConfig values. +// TokenCredentialFromAADConfig attempts to construct a Token using the AADConfig values. // It detects credentials in the following order: // // - azidentity.ClientSecretCredential when `tenantId`, `clientId` and @@ -64,53 +79,40 @@ type AZConfig struct { // // If no set of credentials is found or the azcore.TokenCredential can not be // created, an error is returned. -func TokenFromAADConfig(c AADConfig) (_ *Token, err error) { - var token azcore.TokenCredential +func TokenCredentialFromAADConfig(c AADConfig) (token azcore.TokenCredential, err error) { if c.TenantID != "" && c.ClientID != "" { if c.ClientSecret != "" { - if token, err = azidentity.NewClientSecretCredential(c.TenantID, c.ClientID, c.ClientSecret, &azidentity.ClientSecretCredentialOptions{ + return azidentity.NewClientSecretCredential(c.TenantID, c.ClientID, c.ClientSecret, &azidentity.ClientSecretCredentialOptions{ ClientOptions: azcore.ClientOptions{ Cloud: c.GetCloudConfig(), }, - }); err != nil { - return - } - return NewToken(token), nil + }) } if c.ClientCertificate != "" { certs, pk, err := azidentity.ParseCertificates([]byte(c.ClientCertificate), []byte(c.ClientCertificatePassword)) if err != nil { return nil, err } - if token, err = azidentity.NewClientCertificateCredential(c.TenantID, c.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{ + return azidentity.NewClientCertificateCredential(c.TenantID, c.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{ SendCertificateChain: c.ClientCertificateSendChain, ClientOptions: azcore.ClientOptions{ Cloud: c.GetCloudConfig(), }, - }); err != nil { - return nil, err - } - return NewToken(token), nil + }) } } switch { case c.Tenant != "" && c.AppID != "" && c.Password != "": - if token, err = azidentity.NewClientSecretCredential(c.Tenant, c.AppID, c.Password, &azidentity.ClientSecretCredentialOptions{ + return azidentity.NewClientSecretCredential(c.Tenant, c.AppID, c.Password, &azidentity.ClientSecretCredentialOptions{ ClientOptions: azcore.ClientOptions{ Cloud: c.GetCloudConfig(), }, - }); err != nil { - return - } - return NewToken(token), nil + }) case c.ClientID != "": - if token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + return azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ ID: azidentity.ClientID(c.ClientID), - }); err != nil { - return - } - return NewToken(token), nil + }) default: return nil, fmt.Errorf("invalid data: requires a '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'", "clientId", "tenantId", "clientId", "clientSecret", "tenantId", "clientId", "clientCertificate") @@ -128,3 +130,24 @@ func (s AADConfig) GetCloudConfig() cloud.Configuration { } return cloud.AzurePublic } + +func decode(b []byte) ([]byte, error) { + reader, enc := utfbom.Skip(bytes.NewReader(b)) + switch enc { + case utfbom.UTF16LittleEndian: + u16 := make([]uint16, (len(b)/2)-1) + err := binary.Read(reader, binary.LittleEndian, &u16) + if err != nil { + return nil, err + } + return []byte(string(utf16.Decode(u16))), nil + case utfbom.UTF16BigEndian: + u16 := make([]uint16, (len(b)/2)-1) + err := binary.Read(reader, binary.BigEndian, &u16) + if err != nil { + return nil, err + } + return []byte(string(utf16.Decode(u16))), nil + } + return io.ReadAll(reader) +} diff --git a/internal/sops/azkv/config_test.go b/internal/sops/azkv/config_test.go index c30b990d8..54153ea8d 100644 --- a/internal/sops/azkv/config_test.go +++ b/internal/sops/azkv/config_test.go @@ -1,8 +1,18 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package azkv @@ -148,16 +158,16 @@ func TestTokenFromAADConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - got, err := TokenFromAADConfig(tt.config) + got, err := TokenCredentialFromAADConfig(tt.config) if tt.wantErr { g.Expect(err).To(HaveOccurred()) - g.Expect(got.token).To(BeNil()) + g.Expect(got).To(BeNil()) return } g.Expect(err).ToNot(HaveOccurred()) - g.Expect(got.token).ToNot(BeNil()) - g.Expect(got.token).To(BeAssignableToTypeOf(tt.want)) + g.Expect(got).ToNot(BeNil()) + g.Expect(got).To(BeAssignableToTypeOf(tt.want)) }) } } diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go deleted file mode 100644 index ce91c5bab..000000000 --- a/internal/sops/azkv/keysource.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package azkv - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/binary" - "fmt" - "io/ioutil" - "time" - "unicode/utf16" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" - "github.com/dimchansky/utfbom" -) - -var ( - // azkvTTL is the duration after which a MasterKey requires rotation. - azkvTTL = time.Hour * 24 * 30 * 6 -) - -// MasterKey is an Azure Key Vault Key used to Encrypt and Decrypt SOPS' -// data key. -// -// The underlying authentication token can be configured using TokenFromAADConfig -// and Token.ApplyToMasterKey(). -type MasterKey struct { - VaultURL string - Name string - Version string - - EncryptedKey string - CreationDate time.Time - - token azcore.TokenCredential -} - -// MasterKeyFromURL creates a new MasterKey from a Vault URL, key name, and key -// version. -func MasterKeyFromURL(url, name, version string) *MasterKey { - key := &MasterKey{ - VaultURL: url, - Name: name, - Version: version, - CreationDate: time.Now().UTC(), - } - return key -} - -// Token is an azcore.TokenCredential used for authenticating towards Azure Key -// Vault. -type Token struct { - token azcore.TokenCredential -} - -// NewToken creates a new Token with the provided azcore.TokenCredential. -func NewToken(token azcore.TokenCredential) *Token { - return &Token{token: token} -} - -// ApplyToMasterKey configures the Token on the provided key. -func (t Token) ApplyToMasterKey(key *MasterKey) { - key.token = t.token -} - -// Encrypt takes a SOPS data key, encrypts it with Azure Key Vault, and stores -// the result in the EncryptedKey field. -func (key *MasterKey) Encrypt(dataKey []byte) error { - c, err := azkeys.NewClient(key.VaultURL, key.token, nil) - if err != nil { - return fmt.Errorf("failed to construct Azure Key Vault crypto client to encrypt data: %w", err) - } - resp, err := c.Encrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationsParameters{ - Algorithm: to.Ptr(azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256), - Value: dataKey, - }, nil) - if err != nil { - return fmt.Errorf("failed to encrypt sops data key with Azure Key Vault key '%s': %w", key.ToString(), err) - } - // This is for compatibility between the SOPS upstream which uses - // a much older Azure SDK, and our implementation which is up-to-date - // with the latest. - encodedEncryptedKey := base64.RawURLEncoding.EncodeToString(resp.Result) - key.SetEncryptedDataKey([]byte(encodedEncryptedKey)) - return nil -} - -// EncryptedDataKey returns the encrypted data key this master key holds. -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// SetEncryptedDataKey sets the encrypted data key for this master key. -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been -// encrypted yet. -func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { - if key.EncryptedKey == "" { - return key.Encrypt(dataKey) - } - return nil -} - -// Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns -// the result. -func (key *MasterKey) Decrypt() ([]byte, error) { - c, err := azkeys.NewClient(key.VaultURL, key.token, nil) - if err != nil { - return nil, fmt.Errorf("failed to construct Azure Key Vault crypto client to decrypt data: %w", err) - } - // This is for compatibility between the SOPS upstream which uses - // a much older Azure SDK, and our implementation which is up-to-date - // with the latest. - rawEncryptedKey, err := base64.RawURLEncoding.DecodeString(key.EncryptedKey) - if err != nil { - return nil, fmt.Errorf("failed to base64 decode Azure Key Vault encrypted key: %w", err) - } - resp, err := c.Decrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationsParameters{ - Algorithm: to.Ptr(azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256), - Value: rawEncryptedKey, - }, nil) - if err != nil { - return nil, fmt.Errorf("failed to decrypt sops data key with Azure Key Vault key '%s': %w", key.ToString(), err) - } - return resp.Result, nil -} - -// NeedsRotation returns whether the data key needs to be rotated or not. -func (key *MasterKey) NeedsRotation() bool { - return time.Since(key.CreationDate) > (azkvTTL) -} - -// ToString converts the key to a string representation. -func (key *MasterKey) ToString() string { - return fmt.Sprintf("%s/keys/%s/%s", key.VaultURL, key.Name, key.Version) -} - -// ToMap converts the MasterKey to a map for serialization purposes. -func (key MasterKey) ToMap() map[string]interface{} { - out := make(map[string]interface{}) - out["vaultUrl"] = key.VaultURL - out["key"] = key.Name - out["version"] = key.Version - out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) - out["enc"] = key.EncryptedKey - return out -} - -func decode(b []byte) ([]byte, error) { - reader, enc := utfbom.Skip(bytes.NewReader(b)) - switch enc { - case utfbom.UTF16LittleEndian: - u16 := make([]uint16, (len(b)/2)-1) - err := binary.Read(reader, binary.LittleEndian, &u16) - if err != nil { - return nil, err - } - return []byte(string(utf16.Decode(u16))), nil - case utfbom.UTF16BigEndian: - u16 := make([]uint16, (len(b)/2)-1) - err := binary.Read(reader, binary.BigEndian, &u16) - if err != nil { - return nil, err - } - return []byte(string(utf16.Decode(u16))), nil - } - return ioutil.ReadAll(reader) -} diff --git a/internal/sops/azkv/keysource_integration_test.go b/internal/sops/azkv/keysource_integration_test.go deleted file mode 100644 index a29c1e6d9..000000000 --- a/internal/sops/azkv/keysource_integration_test.go +++ /dev/null @@ -1,147 +0,0 @@ -//go:build integration -// +build integration - -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package azkv - -import ( - "context" - "encoding/base64" - "os" - "testing" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" - . "github.com/onsi/gomega" - "go.mozilla.org/sops/v3/azkv" -) - -// The following values should be created based on the instructions in: -// https://github.com/mozilla/sops#encrypting-using-azure-key-vault -var ( - testVaultURL = os.Getenv("TEST_AZURE_VAULT_URL") - testVaultKeyName = os.Getenv("TEST_AZURE_VAULT_KEY_NAME") - testVaultKeyVersion = os.Getenv("TEST_AZURE_VAULT_KEY_VERSION") - testAADConfig = AADConfig{ - TenantID: os.Getenv("TEST_AZURE_TENANT_ID"), - ClientID: os.Getenv("TEST_AZURE_CLIENT_ID"), - ClientSecret: os.Getenv("TEST_AZURE_CLIENT_SECRET"), - } -) - -func TestMasterKey_Encrypt(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) - token, err := TokenFromAADConfig(testAADConfig) - g.Expect(err).ToNot(HaveOccurred()) - token.ApplyToMasterKey(key) - - g.Expect(key.Encrypt([]byte("foo"))).To(Succeed()) - g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty()) -} - -func TestMasterKey_Decrypt(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) - token, err := TokenFromAADConfig(testAADConfig) - g.Expect(err).ToNot(HaveOccurred()) - token.ApplyToMasterKey(key) - - dataKey := []byte("this is super secret data") - c, err := azkeys.NewClient(key.VaultURL, key.token, nil) - g.Expect(err).ToNot(HaveOccurred()) - resp, err := c.Encrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationsParameters{ - Algorithm: to.Ptr(azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256), - Value: dataKey, - }, nil) - g.Expect(err).ToNot(HaveOccurred()) - key.EncryptedKey = base64.RawURLEncoding.EncodeToString(resp.Result) - g.Expect(key.EncryptedKey).ToNot(BeEmpty()) - g.Expect(key.EncryptedKey).ToNot(Equal(dataKey)) - - got, err := key.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(got).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) - token, err := TokenFromAADConfig(testAADConfig) - g.Expect(err).ToNot(HaveOccurred()) - token.ApplyToMasterKey(key) - - dataKey := []byte("some-data-that-should-be-secret") - - g.Expect(key.Encrypt(dataKey)).To(Succeed()) - g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty()) - - dec, err := key.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - encryptKey := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) - token, err := TokenFromAADConfig(testAADConfig) - g.Expect(err).ToNot(HaveOccurred()) - token.ApplyToMasterKey(encryptKey) - - dataKey := []byte("foo") - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - t.Setenv("AZURE_CLIENT_ID", testAADConfig.ClientID) - t.Setenv("AZURE_TENANT_ID", testAADConfig.TenantID) - t.Setenv("AZURE_CLIENT_SECRET", testAADConfig.ClientSecret) - - decryptKey := &azkv.MasterKey{ - VaultURL: testVaultURL, - Name: testVaultKeyName, - Version: testVaultKeyVersion, - EncryptedKey: encryptKey.EncryptedKey, - CreationDate: time.Now(), - } - - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - t.Setenv("AZURE_CLIENT_ID", testAADConfig.ClientID) - t.Setenv("AZURE_TENANT_ID", testAADConfig.TenantID) - t.Setenv("AZURE_CLIENT_SECRET", testAADConfig.ClientSecret) - - dataKey := []byte("foo") - - encryptKey := &azkv.MasterKey{ - VaultURL: testVaultURL, - Name: testVaultKeyName, - Version: testVaultKeyVersion, - CreationDate: time.Now(), - } - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - decryptKey := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) - token, err := TokenFromAADConfig(testAADConfig) - g.Expect(err).ToNot(HaveOccurred()) - token.ApplyToMasterKey(decryptKey) - - decryptKey.EncryptedKey = encryptKey.EncryptedKey - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} diff --git a/internal/sops/azkv/keysource_test.go b/internal/sops/azkv/keysource_test.go deleted file mode 100644 index 39c19aab4..000000000 --- a/internal/sops/azkv/keysource_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package azkv - -import ( - "testing" - "time" - - . "github.com/onsi/gomega" -) - -func TestToken_ApplyToMasterKey(t *testing.T) { - g := NewWithT(t) - - token, err := TokenFromAADConfig( - AADConfig{TenantID: "tenant", ClientID: "client", ClientSecret: "secret"}, - ) - g.Expect(err).ToNot(HaveOccurred()) - - key := &MasterKey{} - token.ApplyToMasterKey(key) - g.Expect(key.token).To(Equal(token.token)) -} - -func TestMasterKey_EncryptedDataKey(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{EncryptedKey: "some key"} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) -} - -func TestMasterKey_SetEncryptedDataKey(t *testing.T) { - g := NewWithT(t) - - encryptedKey := []byte("encrypted") - key := &MasterKey{} - key.SetEncryptedDataKey(encryptedKey) - g.Expect(key.EncryptedKey).To(BeEquivalentTo(encryptedKey)) -} - -func TestMasterKey_NeedsRotation(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromURL("", "", "") - g.Expect(key.NeedsRotation()).To(BeFalse()) - - key.CreationDate = key.CreationDate.Add(-(azkvTTL + time.Second)) - g.Expect(key.NeedsRotation()).To(BeTrue()) -} - -func TestMasterKey_ToString(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromURL("https://myvault.vault.azure.net", "key-name", "key-version") - g.Expect(key.ToString()).To(Equal("https://myvault.vault.azure.net/keys/key-name/key-version")) -} - -func TestMasterKey_ToMap(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromURL("https://myvault.vault.azure.net", "key-name", "key-version") - key.EncryptedKey = "data" - g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ - "vaultUrl": key.VaultURL, - "key": key.Name, - "version": key.Version, - "created_at": key.CreationDate.UTC().Format(time.RFC3339), - "enc": key.EncryptedKey, - })) -} diff --git a/internal/sops/gcpkms/keysource.go b/internal/sops/gcpkms/keysource.go deleted file mode 100644 index 575e4a3fb..000000000 --- a/internal/sops/gcpkms/keysource.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package gcpkms - -import ( - "context" - "encoding/base64" - "fmt" - "regexp" - "time" - - kms "cloud.google.com/go/kms/apiv1" - "google.golang.org/api/option" - kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" - "google.golang.org/grpc" -) - -var ( - // gcpkmsTTL is the duration after which a MasterKey requires rotation. - gcpkmsTTL = time.Hour * 24 * 30 * 6 -) - -// CredentialJSON is the service account keys used for authentication towards -// GCP KMS. -type CredentialJSON []byte - -// ApplyToMasterKey configures the CredentialJSON on the provided key. -func (c CredentialJSON) ApplyToMasterKey(key *MasterKey) { - key.credentialJSON = c -} - -// MasterKey is a GCP KMS key used to encrypt and decrypt the SOPS -// data key. -// Adapted from https://github.com/mozilla/sops/blob/v3.7.2/gcpkms/keysource.go -// to be able to have fine-grain control over the credentials used to authenticate -// towards GCP KMS. -type MasterKey struct { - // ResourceID is the resource id used to refer to the gcp kms key. - // It can be retrieved using the `gcloud` command. - ResourceID string - // EncryptedKey is the string returned after encrypting with GCP KMS. - EncryptedKey string - // CreationDate is the creation timestamp of the MasterKey. Used - // for NeedsRotation. - CreationDate time.Time - - // credentialJSON are the service account keys used to authenticate - // towards GCP KMS. - credentialJSON []byte - // grpcConn can be used to inject a custom GCP client connection. - // Mostly useful for testing at present, to wire the client to a mock - // server. - grpcConn *grpc.ClientConn -} - -// MasterKeyFromResourceID creates a new MasterKey with the provided resource -// ID. -func MasterKeyFromResourceID(resourceID string) *MasterKey { - return &MasterKey{ - ResourceID: resourceID, - CreationDate: time.Now().UTC(), - } -} - -// Encrypt takes a SOPS data key, encrypts it with GCP KMS, and stores the -// result in the EncryptedKey field. -func (key *MasterKey) Encrypt(datakey []byte) error { - cloudkmsService, err := key.newKMSClient() - if err != nil { - return err - } - defer cloudkmsService.Close() - - req := &kmspb.EncryptRequest{ - Name: key.ResourceID, - Plaintext: datakey, - } - ctx := context.Background() - resp, err := cloudkmsService.Encrypt(ctx, req) - if err != nil { - return fmt.Errorf("failed to encrypt sops data key with GCP KMS: %w", err) - } - key.EncryptedKey = base64.StdEncoding.EncodeToString(resp.Ciphertext) - return nil -} - -// SetEncryptedDataKey sets the encrypted data key for this master key. -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -// EncryptedDataKey returns the encrypted data key this master key holds. -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been -// encrypted yet. -func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { - if key.EncryptedKey == "" { - return key.Encrypt(dataKey) - } - return nil -} - -// Decrypt decrypts the EncryptedKey field with GCP KMS and returns -// the result. -func (key *MasterKey) Decrypt() ([]byte, error) { - service, err := key.newKMSClient() - if err != nil { - return nil, err - } - defer service.Close() - - decodedCipher, err := base64.StdEncoding.DecodeString(string(key.EncryptedDataKey())) - if err != nil { - return nil, err - } - req := &kmspb.DecryptRequest{ - Name: key.ResourceID, - Ciphertext: decodedCipher, - } - ctx := context.Background() - resp, err := service.Decrypt(ctx, req) - if err != nil { - return nil, fmt.Errorf("failed to decrypt sops data key with GCP KMS Key: %w", err) - } - - return resp.Plaintext, nil -} - -// NeedsRotation returns whether the data key needs to be rotated or not. -func (key *MasterKey) NeedsRotation() bool { - return time.Since(key.CreationDate) > (gcpkmsTTL) -} - -// ToString converts the key to a string representation. -func (key *MasterKey) ToString() string { - return key.ResourceID -} - -// ToMap converts the MasterKey to a map for serialization purposes. -func (key MasterKey) ToMap() map[string]interface{} { - out := make(map[string]interface{}) - out["resource_id"] = key.ResourceID - out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) - out["enc"] = key.EncryptedKey - return out -} - -// newKMSClient returns a GCP KMS client configured with the credentialJSON -// and/or grpcConn, falling back to environmental defaults. -// It returns an error if the ResourceID is invalid, or if the client setup -// fails. -func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { - re := regexp.MustCompile(`^projects/[^/]+/locations/[^/]+/keyRings/[^/]+/cryptoKeys/[^/]+$`) - matches := re.FindStringSubmatch(key.ResourceID) - if matches == nil { - return nil, fmt.Errorf("no valid resourceId found in %q", key.ResourceID) - } - - var opts []option.ClientOption - if key.credentialJSON != nil { - opts = append(opts, option.WithCredentialsJSON(key.credentialJSON)) - } - if key.grpcConn != nil { - opts = append(opts, option.WithGRPCConn(key.grpcConn)) - } - - ctx := context.Background() - client, err := kms.NewKeyManagementClient(ctx, opts...) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/sops/gcpkms/keysource_integration_test.go b/internal/sops/gcpkms/keysource_integration_test.go deleted file mode 100644 index e8412a4b4..000000000 --- a/internal/sops/gcpkms/keysource_integration_test.go +++ /dev/null @@ -1,148 +0,0 @@ -//go:build integration && disabled -// +build integration,disabled - -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package gcpkms - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "testing" - - . "github.com/onsi/gomega" - "go.mozilla.org/sops/v3/gcpkms" - - "google.golang.org/api/option" - kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - kms "cloud.google.com/go/kms/apiv1" -) - -var ( - project = os.Getenv("TEST_PROJECT") - testKeyring = os.Getenv("TEST_KEYRING") - testKey = os.Getenv("TEST_CRYPTO_KEY") - testCredsJSON = os.Getenv("TEST_CRED_JSON") - resourceID = fmt.Sprintf("projects/%s/locations/global/keyRings/%s/cryptoKeys/%s", - project, testKeyring, testKey) -) - -func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", testCredsJSON) - - g.Expect(createKMSKeyIfNotExists(resourceID)).To(Succeed()) - - dataKey := []byte("blue golden light") - encryptedKey := gcpkms.NewMasterKeyFromResourceID(resourceID) - g.Expect(encryptedKey.Encrypt(dataKey)).To(Succeed()) - - decryptionKey := MasterKeyFromResourceID(resourceID) - creds, err := ioutil.ReadFile(testCredsJSON) - g.Expect(err).ToNot(HaveOccurred()) - decryptionKey.EncryptedKey = encryptedKey.EncryptedKey - decryptionKey.credentialJSON = creds - dec, err := decryptionKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", testCredsJSON) - g := NewWithT(t) - - g.Expect(createKMSKeyIfNotExists(resourceID)).To(Succeed()) - - dataKey := []byte("silver golden lights") - - encryptionKey := MasterKeyFromResourceID(resourceID) - creds, err := ioutil.ReadFile(testCredsJSON) - g.Expect(err).ToNot(HaveOccurred()) - encryptionKey.credentialJSON = creds - err = encryptionKey.Encrypt(dataKey) - g.Expect(err).ToNot(HaveOccurred()) - - decryptionKey := gcpkms.NewMasterKeyFromResourceID(resourceID) - decryptionKey.EncryptedKey = encryptionKey.EncryptedKey - dec, err := decryptionKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { - g := NewWithT(t) - - g.Expect(createKMSKeyIfNotExists(resourceID)).To(Succeed()) - - key := MasterKeyFromResourceID(resourceID) - creds, err := ioutil.ReadFile(testCredsJSON) - g.Expect(err).ToNot(HaveOccurred()) - key.credentialJSON = creds - - datakey := []byte("a thousand splendid sons") - g.Expect(key.Encrypt(datakey)).To(Succeed()) - g.Expect(key.EncryptedKey).ToNot(BeEmpty()) - - dec, err := key.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(datakey)) -} - -func createKMSKeyIfNotExists(resourceID string) error { - ctx := context.Background() - // check if crypto key exists if not create it - c, err := kms.NewKeyManagementClient(ctx, option.WithCredentialsFile(testCredsJSON)) - if err != nil { - return fmt.Errorf("err creating client: %q", err) - } - - getCryptoKeyReq := &kmspb.GetCryptoKeyRequest{ - Name: resourceID, - } - _, err = c.GetCryptoKey(ctx, getCryptoKeyReq) - if err == nil { - return nil - } - - e, ok := status.FromError(err) - if !ok || (ok && e.Code() != codes.NotFound) { - return fmt.Errorf("err getting crypto key: %q", err) - } - - projectID := fmt.Sprintf("projects/%s/locations/global", project) - createKeyRingReq := &kmspb.CreateKeyRingRequest{ - Parent: projectID, - KeyRingId: testKeyring, - } - - _, err = c.CreateKeyRing(ctx, createKeyRingReq) - e, ok = status.FromError(err) - if err != nil && !(ok && e.Code() == codes.AlreadyExists) { - return fmt.Errorf("err creating key ring: %q", err) - } - - keyRingName := fmt.Sprintf("%s/keyRings/%s", projectID, testKeyring) - keyReq := &kmspb.CreateCryptoKeyRequest{ - Parent: keyRingName, - CryptoKeyId: testKey, - CryptoKey: &kmspb.CryptoKey{ - Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT, - }, - } - _, err = c.CreateCryptoKey(ctx, keyReq) - e, ok = status.FromError(err) - if err != nil && !(ok && e.Code() == codes.AlreadyExists) { - return fmt.Errorf("err creating crypto key: %q", err) - } - - return nil -} diff --git a/internal/sops/gcpkms/keysource_test.go b/internal/sops/gcpkms/keysource_test.go deleted file mode 100644 index b5ce5eb8b..000000000 --- a/internal/sops/gcpkms/keysource_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package gcpkms - -import ( - "encoding/base64" - "fmt" - "log" - "net" - "testing" - "time" - - . "github.com/onsi/gomega" - kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" - "google.golang.org/grpc" -) - -var ( - testResourceID = "projects/test-flux/locations/global/keyRings/test-flux/cryptoKeys/sops" - decryptedData = "decrypted data" - encryptedData = "encrypted data" -) - -func TestMasterKey_EncryptedDataKey(t *testing.T) { - g := NewWithT(t) - key := MasterKey{EncryptedKey: encryptedData} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(encryptedData)) -} - -func TestMasterKey_SetEncryptedDataKey(t *testing.T) { - g := NewWithT(t) - enc := "encrypted key" - key := &MasterKey{} - key.SetEncryptedDataKey([]byte(enc)) - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(enc)) -} - -func TestMasterKey_EncryptIfNeeded(t *testing.T) { - g := NewWithT(t) - key := MasterKey{EncryptedKey: "encrypted key"} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) - - err := key.EncryptIfNeeded([]byte("sops data key")) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) -} - -func TestMasterKey_ToString(t *testing.T) { - rsrcId := testResourceID - g := NewWithT(t) - key := MasterKeyFromResourceID(rsrcId) - g.Expect(key.ToString()).To(Equal(rsrcId)) -} - -func TestMasterKey_ToMap(t *testing.T) { - g := NewWithT(t) - key := MasterKey{ - credentialJSON: []byte("sensitive creds"), - CreationDate: time.Date(2016, time.October, 31, 10, 0, 0, 0, time.UTC), - ResourceID: testResourceID, - EncryptedKey: "this is encrypted", - } - g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ - "resource_id": testResourceID, - "enc": "this is encrypted", - "created_at": "2016-10-31T10:00:00Z", - })) -} - -func TestMasterKey_createCloudKMSService(t *testing.T) { - g := NewWithT(t) - - tests := []struct { - key MasterKey - errString string - }{ - { - key: MasterKey{ - ResourceID: "/projects", - credentialJSON: []byte("some secret"), - }, - errString: "no valid resourceId", - }, - { - key: MasterKey{ - ResourceID: testResourceID, - credentialJSON: []byte(`{ "client_id": ".apps.googleusercontent.com", - "client_secret": "", - "type": "authorized_user"}`), - }, - }, - } - - for _, tt := range tests { - _, err := tt.key.newKMSClient() - if tt.errString != "" { - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring(tt.errString)) - } else { - g.Expect(err).To(BeNil()) - } - } -} - -func TestMasterKey_Decrypt(t *testing.T) { - g := NewWithT(t) - - mockKeyManagement.err = nil - mockKeyManagement.reqs = nil - mockKeyManagement.resps = append(mockKeyManagement.resps[:0], &kmspb.DecryptResponse{ - Plaintext: []byte(decryptedData), - }) - key := MasterKey{ - grpcConn: newGRPCServer("0"), - ResourceID: testResourceID, - EncryptedKey: "encryptedKey", - } - data, err := key.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(data).To(BeEquivalentTo(decryptedData)) -} - -func TestMasterKey_Encrypt(t *testing.T) { - g := NewWithT(t) - - mockKeyManagement.err = nil - mockKeyManagement.reqs = nil - mockKeyManagement.resps = append(mockKeyManagement.resps[:0], &kmspb.EncryptResponse{ - Ciphertext: []byte(encryptedData), - }) - - key := MasterKey{ - grpcConn: newGRPCServer("0"), - ResourceID: testResourceID, - } - err := key.Encrypt([]byte("encrypt")) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(base64.StdEncoding.EncodeToString([]byte(encryptedData)))) -} - -var ( - mockKeyManagement mockKeyManagementServer -) - -func newGRPCServer(port string) *grpc.ClientConn { - serv := grpc.NewServer() - kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement) - - lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%s", port)) - if err != nil { - log.Fatal(err) - } - go serv.Serve(lis) - - conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure()) - if err != nil { - log.Fatal(err) - } - - return conn -} diff --git a/internal/sops/gcpkms/mock_kms_server_test.go b/internal/sops/gcpkms/mock_kms_server_test.go deleted file mode 100644 index fa8ad3b63..000000000 --- a/internal/sops/gcpkms/mock_kms_server_test.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Code generated by gapic-generator. DO NOT EDIT. - -// Ref: https://github.com/googleapis/google-cloud-go/blob/4fe86a327f97ada275ce1744459129df38f9c95b/kms/apiv1/mock_test.go - -package gcpkms - -import ( - "context" - "fmt" - "io" - "strings" - - kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" - - status "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc/metadata" -) - -var _ = io.EOF -var _ = anypb.New -var _ status.Status - -type mockKeyManagementServer struct { - // Embed for forward compatibility. - // Tests will keep working if more methods are added - // in the future. - kmspb.KeyManagementServiceServer - - reqs []proto.Message - - // If set, all calls return this error. - err error - - // responses to return if err == nil - resps []proto.Message -} - -func (s *mockKeyManagementServer) ListKeyRings(ctx context.Context, req *kmspb.ListKeyRingsRequest) (*kmspb.ListKeyRingsResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.ListKeyRingsResponse), nil -} - -func (s *mockKeyManagementServer) ListCryptoKeys(ctx context.Context, req *kmspb.ListCryptoKeysRequest) (*kmspb.ListCryptoKeysResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.ListCryptoKeysResponse), nil -} - -func (s *mockKeyManagementServer) ListCryptoKeyVersions(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest) (*kmspb.ListCryptoKeyVersionsResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.ListCryptoKeyVersionsResponse), nil -} - -func (s *mockKeyManagementServer) ListImportJobs(ctx context.Context, req *kmspb.ListImportJobsRequest) (*kmspb.ListImportJobsResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.ListImportJobsResponse), nil -} - -func (s *mockKeyManagementServer) GetKeyRing(ctx context.Context, req *kmspb.GetKeyRingRequest) (*kmspb.KeyRing, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.KeyRing), nil -} - -func (s *mockKeyManagementServer) GetCryptoKey(ctx context.Context, req *kmspb.GetCryptoKeyRequest) (*kmspb.CryptoKey, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKey), nil -} - -func (s *mockKeyManagementServer) GetCryptoKeyVersion(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKeyVersion), nil -} - -func (s *mockKeyManagementServer) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest) (*kmspb.PublicKey, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.PublicKey), nil -} - -func (s *mockKeyManagementServer) GetImportJob(ctx context.Context, req *kmspb.GetImportJobRequest) (*kmspb.ImportJob, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.ImportJob), nil -} - -func (s *mockKeyManagementServer) CreateKeyRing(ctx context.Context, req *kmspb.CreateKeyRingRequest) (*kmspb.KeyRing, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.KeyRing), nil -} - -func (s *mockKeyManagementServer) CreateCryptoKey(ctx context.Context, req *kmspb.CreateCryptoKeyRequest) (*kmspb.CryptoKey, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKey), nil -} - -func (s *mockKeyManagementServer) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKeyVersion), nil -} - -func (s *mockKeyManagementServer) ImportCryptoKeyVersion(ctx context.Context, req *kmspb.ImportCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKeyVersion), nil -} - -func (s *mockKeyManagementServer) CreateImportJob(ctx context.Context, req *kmspb.CreateImportJobRequest) (*kmspb.ImportJob, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.ImportJob), nil -} - -func (s *mockKeyManagementServer) UpdateCryptoKey(ctx context.Context, req *kmspb.UpdateCryptoKeyRequest) (*kmspb.CryptoKey, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKey), nil -} - -func (s *mockKeyManagementServer) UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKeyVersion), nil -} - -func (s *mockKeyManagementServer) Encrypt(ctx context.Context, req *kmspb.EncryptRequest) (*kmspb.EncryptResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.EncryptResponse), nil -} - -func (s *mockKeyManagementServer) Decrypt(ctx context.Context, req *kmspb.DecryptRequest) (*kmspb.DecryptResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.DecryptResponse), nil -} - -func (s *mockKeyManagementServer) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest) (*kmspb.AsymmetricSignResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.AsymmetricSignResponse), nil -} - -func (s *mockKeyManagementServer) AsymmetricDecrypt(ctx context.Context, req *kmspb.AsymmetricDecryptRequest) (*kmspb.AsymmetricDecryptResponse, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.AsymmetricDecryptResponse), nil -} - -func (s *mockKeyManagementServer) UpdateCryptoKeyPrimaryVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyPrimaryVersionRequest) (*kmspb.CryptoKey, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKey), nil -} - -func (s *mockKeyManagementServer) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.DestroyCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKeyVersion), nil -} - -func (s *mockKeyManagementServer) RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest) (*kmspb.CryptoKeyVersion, error) { - md, _ := metadata.FromIncomingContext(ctx) - if xg := md["x-goog-api-client"]; len(xg) == 0 || !strings.Contains(xg[0], "gl-go/") { - return nil, fmt.Errorf("x-goog-api-client = %v, expected gl-go key", xg) - } - s.reqs = append(s.reqs, req) - if s.err != nil { - return nil, s.err - } - return s.resps[0].(*kmspb.CryptoKeyVersion), nil -} diff --git a/internal/sops/hcvault/keysource.go b/internal/sops/hcvault/keysource.go deleted file mode 100644 index cfbfde028..000000000 --- a/internal/sops/hcvault/keysource.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (C) 2020 The Mozilla SOPS authors -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package hcvault - -import ( - "encoding/base64" - "fmt" - "path" - "time" - - "github.com/hashicorp/vault/api" -) - -var ( - // vaultTTL is the duration after which a MasterKey requires rotation. - vaultTTL = time.Hour * 24 * 30 * 6 -) - -// VaultToken used for authenticating towards a Vault server. -type VaultToken string - -// ApplyToMasterKey configures the token on the provided key. -func (t VaultToken) ApplyToMasterKey(key *MasterKey) { - key.vaultToken = string(t) -} - -// MasterKey is a Vault Transit backend path used to Encrypt and Decrypt -// SOPS' data key. -// -// Adapted from https://github.com/mozilla/sops/blob/v3.7.1/hcvault/keysource.go -// to be able to have fine-grain control over the used decryption keys -// without relying on the existence of environment variable or file. -type MasterKey struct { - KeyName string - EnginePath string - VaultAddress string - - EncryptedKey string - CreationDate time.Time - - vaultToken string -} - -// MasterKeyFromAddress creates a new MasterKey from a Vault address, Transit -// backend path and a key name. -func MasterKeyFromAddress(address, enginePath, keyName string) *MasterKey { - key := &MasterKey{ - VaultAddress: address, - EnginePath: enginePath, - KeyName: keyName, - CreationDate: time.Now().UTC(), - } - return key -} - -// Encrypt takes a SOPS data key, encrypts it with Vault Transit, and stores -// the result in the EncryptedKey field. -func (key *MasterKey) Encrypt(dataKey []byte) error { - client, err := vaultClient(key.VaultAddress, key.vaultToken) - if err != nil { - return err - } - - fullPath := key.encryptPath() - secret, err := client.Logical().Write(fullPath, encryptPayload(dataKey)) - if err != nil { - return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err) - } - encryptedKey, err := encryptedKeyFromSecret(secret) - if err != nil { - return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err) - } - key.EncryptedKey = encryptedKey - return nil -} - -// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been -// encrypted yet. -func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { - if key.EncryptedKey == "" { - return key.Encrypt(dataKey) - } - return nil -} - -// EncryptedDataKey returns the encrypted data key this master key holds. -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// SetEncryptedDataKey sets the encrypted data key for this master key. -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -// Decrypt decrypts the EncryptedKey field with Vault Transit and returns the result. -func (key *MasterKey) Decrypt() ([]byte, error) { - client, err := vaultClient(key.VaultAddress, key.vaultToken) - if err != nil { - return nil, err - } - - fullPath := key.decryptPath() - secret, err := client.Logical().Write(fullPath, decryptPayload(key.EncryptedKey)) - if err != nil { - return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err) - } - dataKey, err := dataKeyFromSecret(secret) - if err != nil { - return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err) - } - return dataKey, nil -} - -// NeedsRotation returns whether the data key needs to be rotated or not. -func (key *MasterKey) NeedsRotation() bool { - // TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data - return time.Since(key.CreationDate) > (vaultTTL) -} - -// ToString converts the key to a string representation. -func (key *MasterKey) ToString() string { - return fmt.Sprintf("%s/v1/%s/keys/%s", key.VaultAddress, key.EnginePath, key.KeyName) -} - -// ToMap converts the MasterKey to a map for serialization purposes. -func (key MasterKey) ToMap() map[string]interface{} { - out := make(map[string]interface{}) - out["vault_address"] = key.VaultAddress - out["key_name"] = key.KeyName - out["engine_path"] = key.EnginePath - out["enc"] = key.EncryptedKey - out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) - return out -} - -// encryptPath returns the path for Encrypt requests. -func (key *MasterKey) encryptPath() string { - return path.Join(key.EnginePath, "encrypt", key.KeyName) -} - -// decryptPath returns the path for Decrypt requests. -func (key *MasterKey) decryptPath() string { - return path.Join(key.EnginePath, "decrypt", key.KeyName) -} - -// encryptPayload returns the payload for an encrypt request of the dataKey. -func encryptPayload(dataKey []byte) map[string]interface{} { - encoded := base64.StdEncoding.EncodeToString(dataKey) - return map[string]interface{}{ - "plaintext": encoded, - } -} - -// encryptedKeyFromSecret attempts to extract the encrypted key from the data -// of the provided secret. -func encryptedKeyFromSecret(secret *api.Secret) (string, error) { - if secret == nil || secret.Data == nil { - return "", fmt.Errorf("transit backend is empty") - } - encrypted, ok := secret.Data["ciphertext"] - if !ok { - return "", fmt.Errorf("no encrypted data") - } - encryptedKey, ok := encrypted.(string) - if !ok { - return "", fmt.Errorf("encrypted ciphertext cannot be cast to string") - } - return encryptedKey, nil -} - -// decryptPayload returns the payload for a decrypt request of the -// encryptedKey. -func decryptPayload(encryptedKey string) map[string]interface{} { - return map[string]interface{}{ - "ciphertext": encryptedKey, - } -} - -// dataKeyFromSecret attempts to extract the data key from the data of the -// provided secret. -func dataKeyFromSecret(secret *api.Secret) ([]byte, error) { - if secret == nil || secret.Data == nil { - return nil, fmt.Errorf("transit backend is empty") - } - decrypted, ok := secret.Data["plaintext"] - if !ok { - return nil, fmt.Errorf("no decrypted data") - } - plaintext, ok := decrypted.(string) - if !ok { - return nil, fmt.Errorf("decrypted plaintext data cannot be cast to string") - } - dataKey, err := base64.StdEncoding.DecodeString(plaintext) - if err != nil { - return nil, fmt.Errorf("cannot decode base64 plaintext into data key bytes") - } - return dataKey, nil -} - -// vaultClient returns a new Vault client, configured with the given address -// and token. -func vaultClient(address, token string) (*api.Client, error) { - cfg := api.DefaultConfig() - cfg.Address = address - client, err := api.NewClient(cfg) - if err != nil { - return nil, fmt.Errorf("cannot create Vault client: %w", err) - } - client.SetToken(token) - return client, nil -} diff --git a/internal/sops/hcvault/keysource_test.go b/internal/sops/hcvault/keysource_test.go deleted file mode 100644 index dfeabf245..000000000 --- a/internal/sops/hcvault/keysource_test.go +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package hcvault - -import ( - "fmt" - logger "log" - "os" - "path" - "testing" - "time" - - "github.com/hashicorp/vault/api" - . "github.com/onsi/gomega" - "github.com/ory/dockertest/v3" - "go.mozilla.org/sops/v3/hcvault" -) - -var ( - // testVaultVersion is the version (image tag) of the Vault server image - // used to test against. - testVaultVersion = "1.10.0" - // testVaultToken is the token of the Vault server. - testVaultToken = "secret" - // testEnginePath is the path to mount the Vault Transit on. - testEnginePath = "sops" - // testVaultAddress is the HTTP/S address of the Vault server, it is set - // by TestMain after booting it. - testVaultAddress string -) - -// TestMain initializes a Vault server using Docker, writes the HTTP address to -// testVaultAddress, waits for it to become ready to serve requests, and enables -// Vault Transit on the testEnginePath. It then runs all the tests, which can -// make use of the various `test*` variables. -func TestMain(m *testing.M) { - // Uses a sensible default on Windows (TCP/HTTP) and Linux/MacOS (socket) - pool, err := dockertest.NewPool("") - if err != nil { - logger.Fatalf("could not connect to docker: %s", err) - } - - // Pull the image, create a container based on it, and run it - resource, err := pool.Run("vault", testVaultVersion, []string{"VAULT_DEV_ROOT_TOKEN_ID=" + testVaultToken}) - if err != nil { - logger.Fatalf("could not start resource: %s", err) - } - - purgeResource := func() { - if err := pool.Purge(resource); err != nil { - logger.Printf("could not purge resource: %s", err) - } - } - - testVaultAddress = fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("8200/tcp")) - // Wait until Vault is ready to serve requests - if err := pool.Retry(func() error { - cfg := api.DefaultConfig() - cfg.Address = testVaultAddress - cli, err := api.NewClient(cfg) - if err != nil { - return fmt.Errorf("cannot create Vault client: %w", err) - } - status, err := cli.Sys().InitStatus() - if err != nil { - return err - } - if status != true { - return fmt.Errorf("waiting on Vault server to become ready") - } - return nil - }); err != nil { - purgeResource() - logger.Fatalf("could not connect to docker: %s", err) - } - - if err = enableVaultTransit(testVaultAddress, testVaultToken, testEnginePath); err != nil { - purgeResource() - logger.Fatalf("could not enable Vault transit: %s", err) - } - - // Run the tests, but only if we succeeded in setting up the Vault server - var code int - if err == nil { - code = m.Run() - } - - // This can't be deferred, as os.Exit simpy does not care - if err := pool.Purge(resource); err != nil { - logger.Fatalf("could not purge resource: %s", err) - } - - os.Exit(code) -} - -func TestMasterKey_Encrypt(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromAddress(testVaultAddress, testEnginePath, "encrypt") - (VaultToken(testVaultToken)).ApplyToMasterKey(key) - g.Expect(createVaultKey(key)).To(Succeed()) - - dataKey := []byte("the majority of your brain is fat") - g.Expect(key.Encrypt(dataKey)).To(Succeed()) - g.Expect(key.EncryptedKey).ToNot(BeEmpty()) - - client, err := vaultClient(key.VaultAddress, key.vaultToken) - g.Expect(err).ToNot(HaveOccurred()) - - payload := decryptPayload(key.EncryptedKey) - secret, err := client.Logical().Write(key.decryptPath(), payload) - g.Expect(err).ToNot(HaveOccurred()) - - decryptedData, err := dataKeyFromSecret(secret) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decryptedData).To(Equal(dataKey)) - - key.EnginePath = "invalid" - g.Expect(key.Encrypt(dataKey)).To(HaveOccurred()) - - key.EnginePath = testEnginePath - key.vaultToken = "" - g.Expect(key.Encrypt(dataKey)).To(HaveOccurred()) -} - -func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - encryptKey := MasterKeyFromAddress(testVaultAddress, testEnginePath, "encrypt-compat") - (VaultToken(testVaultToken)).ApplyToMasterKey(encryptKey) - g.Expect(createVaultKey(encryptKey)).To(Succeed()) - - dataKey := []byte("foo") - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - t.Setenv("VAULT_ADDR", testVaultAddress) - t.Setenv("VAULT_TOKEN", testVaultToken) - decryptKey := hcvault.NewMasterKey(testVaultAddress, testEnginePath, "encrypt-compat") - decryptKey.EncryptedKey = encryptKey.EncryptedKey - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptIfNeeded(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromAddress(testVaultAddress, testEnginePath, "encrypt-if-needed") - (VaultToken(testVaultToken)).ApplyToMasterKey(key) - g.Expect(createVaultKey(key)).To(Succeed()) - - g.Expect(key.EncryptIfNeeded([]byte("data"))).To(Succeed()) - - encryptedKey := key.EncryptedKey - g.Expect(encryptedKey).ToNot(BeEmpty()) - - g.Expect(key.EncryptIfNeeded([]byte("some other data"))).To(Succeed()) - g.Expect(key.EncryptedKey).To(Equal(encryptedKey)) -} - -func TestMasterKey_EncryptedDataKey(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{EncryptedKey: "some key"} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) -} - -func TestMasterKey_Decrypt(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromAddress(testVaultAddress, testEnginePath, "decrypt") - (VaultToken(testVaultToken)).ApplyToMasterKey(key) - g.Expect(createVaultKey(key)).To(Succeed()) - - client, err := vaultClient(key.VaultAddress, key.vaultToken) - g.Expect(err).ToNot(HaveOccurred()) - - dataKey := []byte("the heart of a shrimp is located in its head") - secret, err := client.Logical().Write(key.encryptPath(), encryptPayload(dataKey)) - g.Expect(err).ToNot(HaveOccurred()) - - encryptedKey, err := encryptedKeyFromSecret(secret) - g.Expect(err).NotTo(HaveOccurred()) - - key.EncryptedKey = encryptedKey - got, err := key.Decrypt() - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(got).To(Equal(dataKey)) - - key.EnginePath = "invalid" - g.Expect(key.Encrypt(dataKey)).To(HaveOccurred()) - - key.EnginePath = testEnginePath - key.vaultToken = "" - g.Expect(key.Encrypt(dataKey)).To(HaveOccurred()) -} - -func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - decryptKey := MasterKeyFromAddress(testVaultAddress, testEnginePath, "decrypt-compat") - (VaultToken(testVaultToken)).ApplyToMasterKey(decryptKey) - g.Expect(createVaultKey(decryptKey)).To(Succeed()) - - dataKey := []byte("foo") - - t.Setenv("VAULT_ADDR", testVaultAddress) - t.Setenv("VAULT_TOKEN", testVaultToken) - encryptKey := hcvault.NewMasterKey(testVaultAddress, testEnginePath, "decrypt-compat") - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - decryptKey.EncryptedKey = encryptKey.EncryptedKey - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { - g := NewWithT(t) - - token := VaultToken(testVaultToken) - - encryptKey := MasterKeyFromAddress(testVaultAddress, testEnginePath, "roundtrip") - token.ApplyToMasterKey(encryptKey) - g.Expect(createVaultKey(encryptKey)).To(Succeed()) - - dataKey := []byte("some people have an extra bone in their knee") - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - g.Expect(encryptKey.EncryptedKey).ToNot(BeEmpty()) - - decryptKey := MasterKeyFromAddress(testVaultAddress, testEnginePath, "roundtrip") - token.ApplyToMasterKey(decryptKey) - decryptKey.EncryptedKey = encryptKey.EncryptedKey - - decryptedData, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decryptedData).To(Equal(dataKey)) -} - -func TestMasterKey_NeedsRotation(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromAddress("", "", "") - g.Expect(key.NeedsRotation()).To(BeFalse()) - - key.CreationDate = key.CreationDate.Add(-(vaultTTL + time.Second)) - g.Expect(key.NeedsRotation()).To(BeTrue()) -} - -func TestMasterKey_ToString(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromAddress("https://example.com", "engine", "key-name") - g.Expect(key.ToString()).To(Equal("https://example.com/v1/engine/keys/key-name")) -} - -func TestMasterKey_ToMap(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{ - KeyName: "test-key", - EnginePath: "engine", - VaultAddress: testVaultAddress, - EncryptedKey: "some-encrypted-key", - } - g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ - "vault_address": key.VaultAddress, - "key_name": key.KeyName, - "engine_path": key.EnginePath, - "enc": key.EncryptedKey, - "created_at": "0001-01-01T00:00:00Z", - })) -} - -func Test_encryptedKeyFromSecret(t *testing.T) { - tests := []struct { - name string - secret *api.Secret - want string - wantErr bool - }{ - {name: "nil secret", secret: nil, wantErr: true}, - {name: "secret with nil data", secret: &api.Secret{Data: nil}, wantErr: true}, - {name: "secret without ciphertext data", secret: &api.Secret{Data: map[string]interface{}{"other": true}}, wantErr: true}, - {name: "ciphertext non string", secret: &api.Secret{Data: map[string]interface{}{"ciphertext": 123}}, wantErr: true}, - {name: "ciphertext data", secret: &api.Secret{Data: map[string]interface{}{"ciphertext": "secret string"}}, want: "secret string"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - got, err := encryptedKeyFromSecret(tt.secret) - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - g.Expect(got).To(BeEmpty()) - return - } - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(got).To(Equal(tt.want)) - }) - } -} - -func Test_dataKeyFromSecret(t *testing.T) { - tests := []struct { - name string - secret *api.Secret - want []byte - wantErr bool - }{ - {name: "nil secret", secret: nil, wantErr: true}, - {name: "secret with nil data", secret: &api.Secret{Data: nil}, wantErr: true}, - {name: "secret without plaintext data", secret: &api.Secret{Data: map[string]interface{}{"other": true}}, wantErr: true}, - {name: "plaintext non string", secret: &api.Secret{Data: map[string]interface{}{"plaintext": 123}}, wantErr: true}, - {name: "plaintext non base64", secret: &api.Secret{Data: map[string]interface{}{"plaintext": "notbase64"}}, wantErr: true}, - {name: "plaintext base64 data", secret: &api.Secret{Data: map[string]interface{}{"plaintext": "Zm9v"}}, want: []byte("foo")}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - got, err := dataKeyFromSecret(tt.secret) - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - g.Expect(got).To(BeNil()) - return - } - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(got).To(Equal(tt.want)) - }) - } -} - -// enableVaultTransit enables the Vault Transit backend on the given enginePath. -func enableVaultTransit(address, token, enginePath string) error { - client, err := vaultClient(address, token) - if err != nil { - return fmt.Errorf("cannot create Vault client: %w", err) - } - - if err = client.Sys().Mount(enginePath, &api.MountInput{ - Type: "transit", - Description: "backend transit used by SOPS", - }); err != nil { - return fmt.Errorf("failed to mount transit on engine path '%s': %w", enginePath, err) - } - return nil -} - -// createVaultKey creates a new RSA-4096 Vault key using the data from the -// provided MasterKey. -func createVaultKey(key *MasterKey) error { - client, err := vaultClient(key.VaultAddress, key.vaultToken) - if err != nil { - return fmt.Errorf("cannot create Vault client: %w", err) - } - - p := path.Join(key.EnginePath, "keys", key.KeyName) - payload := make(map[string]interface{}) - payload["type"] = "rsa-4096" - if _, err = client.Logical().Write(p, payload); err != nil { - return err - } - - _, err = client.Logical().Read(p) - return err -} diff --git a/internal/sops/keyservice/keyservice.go b/internal/sops/keyservice/keyservice.go deleted file mode 100644 index 4d2dda569..000000000 --- a/internal/sops/keyservice/keyservice.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package keyservice - -import ( - "go.mozilla.org/sops/v3/age" - "go.mozilla.org/sops/v3/keys" - "go.mozilla.org/sops/v3/pgp" -) - -// IsOfflineMethod returns true for offline decrypt methods or false otherwise -func IsOfflineMethod(mk keys.MasterKey) bool { - switch mk.(type) { - case *pgp.MasterKey, *age.MasterKey: - return true - default: - return false - } -} diff --git a/internal/sops/keyservice/options.go b/internal/sops/keyservice/options.go index ff7d5c41e..91221cb73 100644 --- a/internal/sops/keyservice/options.go +++ b/internal/sops/keyservice/options.go @@ -1,21 +1,35 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package keyservice import ( extage "filippo.io/age" - "github.com/fluxcd/kustomize-controller/internal/sops/gcpkms" - "go.mozilla.org/sops/v3/keyservice" - - "github.com/fluxcd/kustomize-controller/internal/sops/age" - "github.com/fluxcd/kustomize-controller/internal/sops/awskms" - "github.com/fluxcd/kustomize-controller/internal/sops/azkv" - "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" - "github.com/fluxcd/kustomize-controller/internal/sops/pgp" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/getsops/sops/v3/age" + "github.com/getsops/sops/v3/azkv" + "github.com/getsops/sops/v3/gcpkms" + "github.com/getsops/sops/v3/hcvault" + "github.com/getsops/sops/v3/keyservice" + awskms "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/pgp" + "golang.org/x/oauth2" + + intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms" ) // ServerOption is some configuration that modifies the Server. @@ -37,7 +51,7 @@ type WithVaultToken string // ApplyToServer applies this configuration to the given Server. func (o WithVaultToken) ApplyToServer(s *Server) { - s.vaultToken = hcvault.VaultToken(o) + s.vaultToken = hcvault.Token(o) } // WithAgeIdentities configures the parsed age identities on the Server. @@ -48,33 +62,38 @@ func (o WithAgeIdentities) ApplyToServer(s *Server) { s.ageIdentities = age.ParsedIdentities(o) } -// WithAWSKeys configures the AWS credentials on the Server -type WithAWSKeys struct { - CredsProvider *awskms.CredsProvider +// WithAWSCredentialsProvider configures the AWS credentials on the Server +type WithAWSCredentialsProvider struct { + CredentialsProvider func(region string) awssdk.CredentialsProvider } // ApplyToServer applies this configuration to the given Server. -func (o WithAWSKeys) ApplyToServer(s *Server) { - s.awsCredsProvider = o.CredsProvider +func (o WithAWSCredentialsProvider) ApplyToServer(s *Server) { + s.awsCredentialsProvider = func(arn string) *awskms.CredentialsProvider { + region := intawskms.GetRegionFromKMSARN(arn) + cp := o.CredentialsProvider(region) + return awskms.NewCredentialsProvider(cp) + } } -// WithGCPCredsJSON configures the GCP service account credentials JSON on the -// Server. -type WithGCPCredsJSON []byte +// WithGCPTokenSource configures the GCP token source on the Server. +type WithGCPTokenSource struct { + TokenSource oauth2.TokenSource +} // ApplyToServer applies this configuration to the given Server. -func (o WithGCPCredsJSON) ApplyToServer(s *Server) { - s.gcpCredsJSON = gcpkms.CredentialJSON(o) +func (o WithGCPTokenSource) ApplyToServer(s *Server) { + s.gcpTokenSource = gcpkms.NewTokenSource(o.TokenSource) } -// WithAzureToken configures the Azure credential token on the Server. -type WithAzureToken struct { - Token *azkv.Token +// WithAzureTokenCredential configures the Azure credential token on the Server. +type WithAzureTokenCredential struct { + TokenCredential azcore.TokenCredential } // ApplyToServer applies this configuration to the given Server. -func (o WithAzureToken) ApplyToServer(s *Server) { - s.azureToken = o.Token +func (o WithAzureTokenCredential) ApplyToServer(s *Server) { + s.azureTokenCredential = azkv.NewTokenCredential(o.TokenCredential) } // WithDefaultServer configures the fallback default server on the Server. diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 5c3190d34..5e4e5acf4 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -1,23 +1,33 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package keyservice import ( "fmt" - "go.mozilla.org/sops/v3/keyservice" + "github.com/getsops/sops/v3/age" + "github.com/getsops/sops/v3/azkv" + "github.com/getsops/sops/v3/gcpkms" + "github.com/getsops/sops/v3/hcvault" + "github.com/getsops/sops/v3/keyservice" + awskms "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/logging" + "github.com/getsops/sops/v3/pgp" "golang.org/x/net/context" - - "github.com/fluxcd/kustomize-controller/internal/sops/age" - "github.com/fluxcd/kustomize-controller/internal/sops/awskms" - "github.com/fluxcd/kustomize-controller/internal/sops/azkv" - "github.com/fluxcd/kustomize-controller/internal/sops/gcpkms" - "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" - "github.com/fluxcd/kustomize-controller/internal/sops/pgp" ) // Server is a key service server that uses SOPS MasterKeys to fulfill @@ -40,22 +50,21 @@ type Server struct { // vaultToken is the token used for Encrypt and Decrypt operations of // Hashicorp Vault requests. // When empty, the request will be handled by defaultServer. - vaultToken hcvault.VaultToken + vaultToken hcvault.Token - // azureToken is the credential token used for Encrypt and Decrypt + // azureTokenCredential is the credential token used for Encrypt and Decrypt // operations of Azure Key Vault requests. // When nil, the request will be handled by defaultServer. - azureToken *azkv.Token + azureTokenCredential *azkv.TokenCredential - // awsCredsProvider is the Credentials object used for Encrypt and Decrypt + // awsCredentialsProvider is the Credentials object used for Encrypt and Decrypt // operations of AWS KMS requests. // When nil, the request will be handled by defaultServer. - awsCredsProvider *awskms.CredsProvider + awsCredentialsProvider func(arn string) *awskms.CredentialsProvider - // gcpCredsJSON is the JSON credentials used for Decrypt and Encrypt - // operations of GCP KMS requests. When nil, a default client with - // environmental runtime settings will be used. - gcpCredsJSON gcpkms.CredentialJSON + // gcpTokenSource is the token source used for Encrypt and Decrypt + // operations of GCP KMS requests. + gcpTokenSource gcpkms.TokenSource // defaultServer is the fallback server, used to handle any request that // is not eligible to be handled by this Server. @@ -71,11 +80,17 @@ func NewServer(options ...ServerOption) keyservice.KeyServiceServer { for _, opt := range options { opt.ApplyToServer(s) } + if s.defaultServer == nil { s.defaultServer = &keyservice.Server{ Prompt: false, } } + + // Effectively disable any logging from the key services. + // 0 equals to panic level, which should never be reached. + logging.SetLevel(0) + return s } @@ -119,15 +134,13 @@ func (ks Server) Encrypt(ctx context.Context, req *keyservice.EncryptRequest) (* Ciphertext: cipherText, }, nil case *keyservice.Key_AzureKeyvaultKey: - if ks.azureToken != nil { - ciphertext, err := ks.encryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Plaintext) - if err != nil { - return nil, err - } - return &keyservice.EncryptResponse{ - Ciphertext: ciphertext, - }, nil + ciphertext, err := ks.encryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Plaintext) + if err != nil { + return nil, err } + return &keyservice.EncryptResponse{ + Ciphertext: ciphertext, + }, nil case *keyservice.Key_GcpKmsKey: ciphertext, err := ks.encryptWithGCPKMS(k.GcpKmsKey, req.Plaintext) if err != nil { @@ -183,15 +196,13 @@ func (ks Server) Decrypt(ctx context.Context, req *keyservice.DecryptRequest) (* Plaintext: plaintext, }, nil case *keyservice.Key_AzureKeyvaultKey: - if ks.azureToken != nil { - plaintext, err := ks.decryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Ciphertext) - if err != nil { - return nil, err - } - return &keyservice.DecryptResponse{ - Plaintext: plaintext, - }, nil + plaintext, err := ks.decryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Ciphertext) + if err != nil { + return nil, err } + return &keyservice.DecryptResponse{ + Plaintext: plaintext, + }, nil case *keyservice.Key_GcpKmsKey: plaintext, err := ks.decryptWithGCPKMS(k.GcpKmsKey, req.Ciphertext) if err != nil { @@ -208,7 +219,8 @@ func (ks Server) Decrypt(ctx context.Context, req *keyservice.DecryptRequest) (* } func (ks *Server) encryptWithPgp(key *keyservice.PgpKey, plaintext []byte) ([]byte, error) { - pgpKey := pgp.MasterKeyFromFingerprint(key.Fingerprint) + pgpKey := pgp.NewMasterKeyFromFingerprint(key.Fingerprint) + pgp.DisableOpenPGP{}.ApplyToMasterKey(pgpKey) if ks.gnuPGHome != "" { ks.gnuPGHome.ApplyToMasterKey(pgpKey) } @@ -220,7 +232,8 @@ func (ks *Server) encryptWithPgp(key *keyservice.PgpKey, plaintext []byte) ([]by } func (ks *Server) decryptWithPgp(key *keyservice.PgpKey, ciphertext []byte) ([]byte, error) { - pgpKey := pgp.MasterKeyFromFingerprint(key.Fingerprint) + pgpKey := pgp.NewMasterKeyFromFingerprint(key.Fingerprint) + pgp.DisableOpenPGP{}.ApplyToMasterKey(pgpKey) if ks.gnuPGHome != "" { ks.gnuPGHome.ApplyToMasterKey(pgpKey) } @@ -279,18 +292,8 @@ func (ks *Server) decryptWithHCVault(key *keyservice.VaultKey, ciphertext []byte } func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([]byte, error) { - context := make(map[string]string) - for key, val := range key.Context { - context[key] = val - } - awsKey := awskms.MasterKey{ - Arn: key.Arn, - Role: key.Role, - EncryptionContext: context, - } - if ks.awsCredsProvider != nil { - ks.awsCredsProvider.ApplyToMasterKey(&awsKey) - } + awsKey := kmsKeyToMasterKey(key) + ks.awsCredentialsProvider(key.Arn).ApplyToMasterKey(&awsKey) if err := awsKey.Encrypt(plaintext); err != nil { return nil, err } @@ -298,20 +301,9 @@ func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([ } func (ks *Server) decryptWithAWSKMS(key *keyservice.KmsKey, cipherText []byte) ([]byte, error) { - context := make(map[string]string) - for key, val := range key.Context { - context[key] = val - } - awsKey := awskms.MasterKey{ - Arn: key.Arn, - Role: key.Role, - EncryptionContext: context, - } + awsKey := kmsKeyToMasterKey(key) awsKey.EncryptedKey = string(cipherText) - - if ks.awsCredsProvider != nil { - ks.awsCredsProvider.ApplyToMasterKey(&awsKey) - } + ks.awsCredentialsProvider(key.Arn).ApplyToMasterKey(&awsKey) return awsKey.Decrypt() } @@ -321,7 +313,7 @@ func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, pla Name: key.Name, Version: key.Version, } - ks.azureToken.ApplyToMasterKey(&azureKey) + ks.azureTokenCredential.ApplyToMasterKey(&azureKey) if err := azureKey.Encrypt(plaintext); err != nil { return nil, err } @@ -334,7 +326,7 @@ func (ks *Server) decryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, cip Name: key.Name, Version: key.Version, } - ks.azureToken.ApplyToMasterKey(&azureKey) + ks.azureTokenCredential.ApplyToMasterKey(&azureKey) azureKey.EncryptedKey = string(ciphertext) plaintext, err := azureKey.Decrypt() return plaintext, err @@ -344,7 +336,7 @@ func (ks *Server) encryptWithGCPKMS(key *keyservice.GcpKmsKey, plaintext []byte) gcpKey := gcpkms.MasterKey{ ResourceID: key.ResourceId, } - ks.gcpCredsJSON.ApplyToMasterKey(&gcpKey) + ks.gcpTokenSource.ApplyToMasterKey(&gcpKey) if err := gcpKey.Encrypt(plaintext); err != nil { return nil, err } @@ -355,8 +347,21 @@ func (ks *Server) decryptWithGCPKMS(key *keyservice.GcpKmsKey, ciphertext []byte gcpKey := gcpkms.MasterKey{ ResourceID: key.ResourceId, } - ks.gcpCredsJSON.ApplyToMasterKey(&gcpKey) + ks.gcpTokenSource.ApplyToMasterKey(&gcpKey) gcpKey.EncryptedKey = string(ciphertext) plaintext, err := gcpKey.Decrypt() return plaintext, err } + +func kmsKeyToMasterKey(key *keyservice.KmsKey) awskms.MasterKey { + ctx := make(map[string]*string) + for k, v := range key.Context { + value := v + ctx[k] = &value + } + return awskms.MasterKey{ + Arn: key.Arn, + Role: key.Role, + EncryptionContext: ctx, + } +} diff --git a/internal/sops/keyservice/server_test.go b/internal/sops/keyservice/server_test.go index fb4da3115..996f6f570 100644 --- a/internal/sops/keyservice/server_test.go +++ b/internal/sops/keyservice/server_test.go @@ -1,8 +1,18 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package keyservice @@ -11,24 +21,26 @@ import ( "os" "testing" + gcpkmsapi "cloud.google.com/go/kms/apiv1" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/getsops/sops/v3/age" + "github.com/getsops/sops/v3/azkv" + "github.com/getsops/sops/v3/gcpkms" + "github.com/getsops/sops/v3/hcvault" + "github.com/getsops/sops/v3/keyservice" + awskms "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/pgp" . "github.com/onsi/gomega" - "go.mozilla.org/sops/v3/keyservice" "golang.org/x/net/context" - - "github.com/fluxcd/kustomize-controller/internal/sops/age" - "github.com/fluxcd/kustomize-controller/internal/sops/awskms" - "github.com/fluxcd/kustomize-controller/internal/sops/azkv" - "github.com/fluxcd/kustomize-controller/internal/sops/gcpkms" - "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" - "github.com/fluxcd/kustomize-controller/internal/sops/pgp" + "golang.org/x/oauth2/google" ) func TestServer_EncryptDecrypt_PGP(t *testing.T) { const ( - mockPublicKey = "../pgp/testdata/public.gpg" - mockPrivateKey = "../pgp/testdata/private.gpg" + mockPublicKey = "testdata/public.gpg" + mockPrivateKey = "testdata/private.gpg" mockFingerprint = "B59DAF469E8C948138901A649732075EA221A7EA" ) @@ -42,7 +54,7 @@ func TestServer_EncryptDecrypt_PGP(t *testing.T) { g.Expect(gnuPGHome.ImportFile(mockPublicKey)).To(Succeed()) s := NewServer(WithGnuPGHome(gnuPGHome)) - key := KeyFromMasterKey(pgp.MasterKeyFromFingerprint(mockFingerprint)) + key := KeyFromMasterKey(pgp.NewMasterKeyFromFingerprint(mockFingerprint)) dataKey := []byte("some data key") encResp, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ Key: &key, @@ -96,7 +108,7 @@ func TestServer_EncryptDecrypt_HCVault(t *testing.T) { g := NewWithT(t) s := NewServer(WithVaultToken("token")) - key := KeyFromMasterKey(hcvault.MasterKeyFromAddress("https://example.com", "engine-path", "key-name")) + key := KeyFromMasterKey(hcvault.NewMasterKey("https://example.com", "engine-path", "key-name")) _, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ Key: &key, }) @@ -116,7 +128,7 @@ func TestServer_EncryptDecrypt_HCVault_Fallback(t *testing.T) { fallback := NewMockKeyServer() s := NewServer(WithDefaultServer{Server: fallback}) - key := KeyFromMasterKey(hcvault.MasterKeyFromAddress("https://example.com", "engine-path", "key-name")) + key := KeyFromMasterKey(hcvault.NewMasterKey("https://example.com", "engine-path", "key-name")) encReq := &keyservice.EncryptRequest{ Key: &key, Plaintext: []byte("some data key"), @@ -134,6 +146,7 @@ func TestServer_EncryptDecrypt_HCVault_Fallback(t *testing.T) { Ciphertext: []byte("some ciphertext"), } _, err = s.Decrypt(context.TODO(), decReq) + g.Expect(err).To(HaveOccurred()) g.Expect(fallback.decryptReqs).To(HaveLen(1)) g.Expect(fallback.decryptReqs).To(ContainElement(decReq)) g.Expect(fallback.encryptReqs).To(HaveLen(0)) @@ -141,8 +154,8 @@ func TestServer_EncryptDecrypt_HCVault_Fallback(t *testing.T) { func TestServer_EncryptDecrypt_awskms(t *testing.T) { g := NewWithT(t) - s := NewServer(WithAWSKeys{ - CredsProvider: awskms.NewCredsProvider(credentials.StaticCredentialsProvider{}), + s := NewServer(WithAWSCredentialsProvider{ + CredentialsProvider: func(region string) aws.CredentialsProvider { return credentials.StaticCredentialsProvider{} }, }) key := KeyFromMasterKey(awskms.NewMasterKeyFromArn("arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", nil, "")) @@ -164,9 +177,9 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) { identity, err := azidentity.NewDefaultAzureCredential(nil) g.Expect(err).ToNot(HaveOccurred()) - s := NewServer(WithAzureToken{Token: azkv.NewToken(identity)}) + s := NewServer(WithAzureTokenCredential{TokenCredential: identity}) - key := KeyFromMasterKey(azkv.MasterKeyFromURL("", "", "")) + key := KeyFromMasterKey(azkv.NewMasterKey("", "", "")) _, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ Key: &key, }) @@ -181,57 +194,27 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) { } -func TestServer_EncryptDecrypt_azkv_Fallback(t *testing.T) { - g := NewWithT(t) - - fallback := NewMockKeyServer() - s := NewServer(WithDefaultServer{Server: fallback}) - - key := KeyFromMasterKey(azkv.MasterKeyFromURL("", "", "")) - encReq := &keyservice.EncryptRequest{ - Key: &key, - Plaintext: []byte("some data key"), - } - _, err := s.Encrypt(context.TODO(), encReq) - g.Expect(err).To(HaveOccurred()) - g.Expect(fallback.encryptReqs).To(HaveLen(1)) - g.Expect(fallback.encryptReqs).To(ContainElement(encReq)) - g.Expect(fallback.decryptReqs).To(HaveLen(0)) - - fallback = NewMockKeyServer() - s = NewServer(WithDefaultServer{Server: fallback}) - - decReq := &keyservice.DecryptRequest{ - Key: &key, - Ciphertext: []byte("some ciphertext"), - } - _, err = s.Decrypt(context.TODO(), decReq) - g.Expect(fallback.decryptReqs).To(HaveLen(1)) - g.Expect(fallback.decryptReqs).To(ContainElement(decReq)) - g.Expect(fallback.encryptReqs).To(HaveLen(0)) -} - func TestServer_EncryptDecrypt_gcpkms(t *testing.T) { g := NewWithT(t) - creds := `{ "client_id": ".apps.googleusercontent.com", - "client_secret": "", - "type": "authorized_user"}` - s := NewServer(WithGCPCredsJSON([]byte(creds))) + creds, err := google.CredentialsFromJSON(context.Background(), + []byte(`{"type":"service_account"}`), gcpkmsapi.DefaultAuthScopes()...) + g.Expect(err).ToNot(HaveOccurred()) + s := NewServer(WithGCPTokenSource{TokenSource: creds.TokenSource}) resourceID := "projects/test-flux/locations/global/keyRings/test-flux/cryptoKeys/sops" - key := KeyFromMasterKey(gcpkms.MasterKeyFromResourceID(resourceID)) - _, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ + key := KeyFromMasterKey(gcpkms.NewMasterKeyFromResourceID(resourceID)) + _, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ Key: &key, }) g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("failed to encrypt sops data key with GCP KMS")) + g.Expect(err.Error()).To(ContainSubstring("failed to encrypt sops data key with GCP KMS key")) _, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{ Key: &key, }) g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("failed to decrypt sops data key with GCP KMS")) + g.Expect(err.Error()).To(ContainSubstring("failed to decrypt sops data key with GCP KMS key")) } diff --git a/internal/sops/pgp/testdata/private.gpg b/internal/sops/keyservice/testdata/private.gpg similarity index 100% rename from internal/sops/pgp/testdata/private.gpg rename to internal/sops/keyservice/testdata/private.gpg diff --git a/internal/sops/pgp/testdata/public.gpg b/internal/sops/keyservice/testdata/public.gpg similarity index 100% rename from internal/sops/pgp/testdata/public.gpg rename to internal/sops/keyservice/testdata/public.gpg diff --git a/internal/sops/keyservice/utils_test.go b/internal/sops/keyservice/utils_test.go index 9bdfb5b43..a4f963f7a 100644 --- a/internal/sops/keyservice/utils_test.go +++ b/internal/sops/keyservice/utils_test.go @@ -1,8 +1,18 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package keyservice @@ -10,15 +20,14 @@ import ( "context" "fmt" - "go.mozilla.org/sops/v3/keys" - "go.mozilla.org/sops/v3/keyservice" - - "github.com/fluxcd/kustomize-controller/internal/sops/age" - "github.com/fluxcd/kustomize-controller/internal/sops/awskms" - "github.com/fluxcd/kustomize-controller/internal/sops/azkv" - "github.com/fluxcd/kustomize-controller/internal/sops/gcpkms" - "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" - "github.com/fluxcd/kustomize-controller/internal/sops/pgp" + "github.com/getsops/sops/v3/age" + "github.com/getsops/sops/v3/azkv" + "github.com/getsops/sops/v3/gcpkms" + "github.com/getsops/sops/v3/hcvault" + "github.com/getsops/sops/v3/keys" + "github.com/getsops/sops/v3/keyservice" + awskms "github.com/getsops/sops/v3/kms" + "github.com/getsops/sops/v3/pgp" ) // KeyFromMasterKey converts a SOPS internal MasterKey to an RPC Key that can diff --git a/internal/sops/pgp/keysource.go b/internal/sops/pgp/keysource.go deleted file mode 100644 index 1d9343df7..000000000 --- a/internal/sops/pgp/keysource.go +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (C) 2016-2020 The Mozilla SOPS authors -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package pgp - -import ( - "bytes" - "fmt" - "io" - "os" - "os/exec" - "os/user" - "path/filepath" - "strings" - "time" -) - -const ( - // SopsGpgExecEnv can be set as an environment variable to overwrite the - // GnuPG binary used. - SopsGpgExecEnv = "FLUX_SOPS_GPG_EXEC" -) - -var ( - // pgpTTL is the duration after which a MasterKey requires rotation. - pgpTTL = time.Hour * 24 * 30 * 6 -) - -// MasterKey is a PGP key used to securely store SOPS' data key by -// encrypting it and decrypting it. -// -// Adapted from https://github.com/mozilla/sops/blob/v3.7.2/pgp/keysource.go -// to be able to control the GPG home directory and have a "contained" -// environment. -// -// We are unable to drop the dependency on the GPG binary (although we -// wish!) because the builtin GPG support in Go is limited, it does for -// example not offer support for FIPS: -// * https://github.com/golang/go/issues/11658#issuecomment-120448974 -// * https://github.com/golang/go/issues/45188 -type MasterKey struct { - // Fingerprint contains the fingerprint of the PGP key used to Encrypt - // or Decrypt the data key with. - Fingerprint string - // EncryptedKey contains the SOPS data key encrypted with PGP. - EncryptedKey string - // CreationDate of the MasterKey, used to determine if the EncryptedKey - // needs rotation. - CreationDate time.Time - - // gnuPGHomeDir contains the absolute path to a GnuPG home directory. - // It can be injected by a (local) keyservice.KeyServiceServer using - // GnuPGHome.ApplyToMasterKey(). - gnuPGHomeDir string -} - -// MasterKeyFromFingerprint takes a PGP fingerprint and returns a -// new MasterKey with that fingerprint. -func MasterKeyFromFingerprint(fingerprint string) *MasterKey { - return &MasterKey{ - Fingerprint: strings.Replace(fingerprint, " ", "", -1), - CreationDate: time.Now().UTC(), - } -} - -// GnuPGHome is the absolute path to a GnuPG home directory. -// A new keyring can be constructed by combining the use of NewGnuPGHome() and -// Import() or ImportFile(). -type GnuPGHome string - -// NewGnuPGHome initializes a new GnuPGHome in a temporary directory. -// The caller is expected to handle the garbage collection of the created -// directory. -func NewGnuPGHome() (GnuPGHome, error) { - tmpDir, err := os.MkdirTemp("", "sops-gnupghome-") - if err != nil { - return "", fmt.Errorf("failed to create new GnuPG home: %w", err) - } - return GnuPGHome(tmpDir), nil -} - -// Import attempts to import the armored key bytes into the GnuPGHome keyring. -// It returns an error if the GnuPGHome does not pass Validate, or if the -// import failed. -func (d GnuPGHome) Import(armoredKey []byte) error { - if err := d.Validate(); err != nil { - return fmt.Errorf("cannot import armored key data into GnuPG keyring: %w", err) - } - - args := []string{"--batch", "--import"} - err, _, stderr := gpgExec(d.String(), args, bytes.NewReader(armoredKey)) - if err != nil { - return fmt.Errorf("failed to import armored key data into GnuPG keyring: %s", strings.TrimSpace(stderr.String())) - } - return nil -} - -// ImportFile attempts to import the armored key file into the GnuPGHome -// keyring. -// It returns an error if the GnuPGHome does not pass Validate, or if the -// import failed. -func (d GnuPGHome) ImportFile(path string) error { - b, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("cannot read armored key data from file: %w", err) - } - return d.Import(b) -} - -// Validate ensures the GnuPGHome is a valid GnuPG home directory path. -// When validation fails, it returns a descriptive reason as error. -func (d GnuPGHome) Validate() error { - if d == "" { - return fmt.Errorf("empty GNUPGHOME path") - } - if !filepath.IsAbs(d.String()) { - return fmt.Errorf("GNUPGHOME must be an absolute path") - } - fi, err := os.Lstat(d.String()) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("GNUPGHOME does not exist") - } - return fmt.Errorf("cannot stat GNUPGHOME: %w", err) - } - if !fi.IsDir() { - return fmt.Errorf("GNUGPHOME is not a directory") - } - if perm := fi.Mode().Perm(); perm != 0o700 { - return fmt.Errorf("GNUPGHOME has invalid permissions: got %#o wanted %#o", perm, 0o700) - } - return nil -} - -// String returns the GnuPGHome as a string. It does not Validate. -func (d GnuPGHome) String() string { - return string(d) -} - -// ApplyToMasterKey configures the GnuPGHome on the provided key if it passes -// Validate. -func (d GnuPGHome) ApplyToMasterKey(key *MasterKey) { - if err := d.Validate(); err == nil { - key.gnuPGHomeDir = d.String() - } -} - -// Encrypt encrypts the data key with the PGP key with the same -// fingerprint as the MasterKey. -func (key *MasterKey) Encrypt(dataKey []byte) error { - fingerprint := shortenFingerprint(key.Fingerprint) - - args := []string{ - "--no-default-recipient", - "--yes", - "--encrypt", - "-a", - "-r", - key.Fingerprint, - "--trusted-key", - fingerprint, - "--no-encrypt-to", - } - err, stdout, stderr := gpgExec(key.gnuPGHome(), args, bytes.NewReader(dataKey)) - if err != nil { - return fmt.Errorf("failed to encrypt sops data key with pgp: %s", strings.TrimSpace(stderr.String())) - } - - key.SetEncryptedDataKey(bytes.TrimSpace(stdout.Bytes())) - return nil -} - -// EncryptIfNeeded encrypts the data key with PGP only if it's needed, -// that is, if it hasn't been encrypted already. -func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { - if key.EncryptedKey == "" { - return key.Encrypt(dataKey) - } - return nil -} - -// EncryptedDataKey returns the encrypted data key this master key holds. -func (key *MasterKey) EncryptedDataKey() []byte { - return []byte(key.EncryptedKey) -} - -// SetEncryptedDataKey sets the encrypted data key for this master key. -func (key *MasterKey) SetEncryptedDataKey(enc []byte) { - key.EncryptedKey = string(enc) -} - -// Decrypt uses PGP to obtain the data key from the EncryptedKey store -// in the MasterKey and returns it. -func (key *MasterKey) Decrypt() ([]byte, error) { - args := []string{ - "-d", - } - err, stdout, stderr := gpgExec(key.gnuPGHome(), args, strings.NewReader(key.EncryptedKey)) - if err != nil { - return nil, fmt.Errorf("failed to decrypt sops data key with pgp: %s", strings.TrimSpace(stderr.String())) - } - return stdout.Bytes(), nil -} - -// NeedsRotation returns whether the data key needs to be rotated -// or not. -func (key *MasterKey) NeedsRotation() bool { - return time.Since(key.CreationDate) > (pgpTTL) -} - -// ToString returns the string representation of the key, i.e. its -// fingerprint. -func (key *MasterKey) ToString() string { - return key.Fingerprint -} - -// ToMap converts the MasterKey into a map for serialization purposes. -func (key MasterKey) ToMap() map[string]interface{} { - out := make(map[string]interface{}) - out["fp"] = key.Fingerprint - out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) - out["enc"] = key.EncryptedKey - return out -} - -// gnuPGHome determines the GnuPG home directory for the MasterKey, and returns -// its path. In order of preference: -// 1. MasterKey.gnuPGHomeDir -// 2. $GNUPGHOME -// 3. user.Current().HomeDir/.gnupg -// 4. $HOME/.gnupg -func (key *MasterKey) gnuPGHome() string { - if key.gnuPGHomeDir == "" { - dir := os.Getenv("GNUPGHOME") - if dir == "" { - usr, err := user.Current() - if err != nil { - return filepath.Join(os.Getenv("HOME"), ".gnupg") - } - return filepath.Join(usr.HomeDir, ".gnupg") - } - return dir - } - return key.gnuPGHomeDir -} - -// gpgExec runs the provided args with the gpgBinary, while restricting it to -// gnuPGHome. Stdout and stderr can be read from the returned buffers. -// When the command fails, an error is returned. -func gpgExec(gnuPGHome string, args []string, stdin io.Reader) (err error, stdout bytes.Buffer, stderr bytes.Buffer) { - if gnuPGHome != "" { - args = append([]string{"--no-default-keyring", "--homedir", gnuPGHome}, args...) - } - - cmd := exec.Command(gpgBinary(), args...) - cmd.Stdin = stdin - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err = cmd.Run() - return -} - -// gpgBinary returns the GnuPG binary which must be used. -// It allows for runtime modifications by setting the environment variable -// SopsGpgExecEnv to the absolute path of the replacement binary. -func gpgBinary() string { - binary := "gpg" - if envBinary := os.Getenv(SopsGpgExecEnv); envBinary != "" && filepath.IsAbs(envBinary) { - binary = envBinary - } - return binary -} - -// shortenFingerprint returns the short ID of the given fingerprint. -// This is mostly used for compatability reasons, as older versions of GnuPG -// do not always like long IDs. -func shortenFingerprint(fingerprint string) string { - if offset := len(fingerprint) - 16; offset > 0 { - fingerprint = fingerprint[offset:] - } - return fingerprint -} diff --git a/internal/sops/pgp/keysource_test.go b/internal/sops/pgp/keysource_test.go deleted file mode 100644 index 4080084e8..000000000 --- a/internal/sops/pgp/keysource_test.go +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright (C) 2022 The Flux authors -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package pgp - -import ( - "bytes" - "os" - "os/user" - "path/filepath" - "strings" - "testing" - "time" - - fuzz "github.com/AdaLogics/go-fuzz-headers" - . "github.com/onsi/gomega" - "go.mozilla.org/sops/v3/pgp" -) - -var ( - mockPublicKey = "testdata/public.gpg" - mockPrivateKey = "testdata/private.gpg" - mockFingerprint = "B59DAF469E8C948138901A649732075EA221A7EA" -) - -func TestMasterKeyFromFingerprint(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromFingerprint(mockFingerprint) - g.Expect(key.Fingerprint).To(Equal(mockFingerprint)) - g.Expect(key.CreationDate).Should(BeTemporally("~", time.Now(), time.Second)) - - key = MasterKeyFromFingerprint("B59DAF 469E8C94813 8901A 649732075E A221A7EA") - g.Expect(key.Fingerprint).To(Equal(mockFingerprint)) -} - -func TestNewGnuPGHome(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).NotTo(HaveOccurred()) - - g.Expect(gnuPGHome.String()).To(BeADirectory()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.Validate()).ToNot(HaveOccurred()) -} - -func TestGnuPGHome_Import(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).NotTo(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - - b, err := os.ReadFile(mockPublicKey) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(gnuPGHome.Import(b)).To(Succeed()) - - err, _, stderr := gpgExec(gnuPGHome.String(), []string{"--list-keys", mockFingerprint}, nil) - g.Expect(err).ToNot(HaveOccurred(), stderr.String()) - - b, err = os.ReadFile(mockPrivateKey) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(gnuPGHome.Import(b)).To(Succeed()) - - err, _, stderr = gpgExec(gnuPGHome.String(), []string{"--list-secret-keys", mockFingerprint}, nil) - g.Expect(err).ToNot(HaveOccurred(), stderr.String()) - - g.Expect(gnuPGHome.Import([]byte("invalid armored data"))).To(HaveOccurred()) - - g.Expect(GnuPGHome("").Import(b)).To(HaveOccurred()) -} - -func TestGnuPGHome_ImportFile(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).NotTo(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - - g.Expect(gnuPGHome.ImportFile(mockPublicKey)).To(Succeed()) - g.Expect(gnuPGHome.ImportFile("invalid")).To(HaveOccurred()) -} - -func TestGnuPGHome_Validate(t *testing.T) { - t.Run("empty path", func(t *testing.T) { - g := NewWithT(t) - - g.Expect(GnuPGHome("").Validate()).To(HaveOccurred()) - }) - - t.Run("relative path", func(t *testing.T) { - g := NewWithT(t) - - g.Expect(GnuPGHome("../../.gnupghome").Validate()).To(HaveOccurred()) - }) - - t.Run("file path", func(t *testing.T) { - g := NewWithT(t) - - tmpDir := t.TempDir() - f, err := os.CreateTemp(tmpDir, "file") - g.Expect(err).ToNot(HaveOccurred()) - defer f.Close() - - g.Expect(GnuPGHome(f.Name()).Validate()).To(HaveOccurred()) - }) - - t.Run("wrong permissions", func(t *testing.T) { - g := NewWithT(t) - - // Is created with 0755 - tmpDir := t.TempDir() - g.Expect(GnuPGHome(tmpDir).Validate()).To(HaveOccurred()) - }) - - t.Run("valid", func(t *testing.T) { - g := NewWithT(t) - - gnupgHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnupgHome.String()) - }) - g.Expect(gnupgHome.Validate()).To(Succeed()) - }) -} - -func TestGnuPGHome_String(t *testing.T) { - g := NewWithT(t) - - gnuPGHome := GnuPGHome("/some/absolute/path") - g.Expect(gnuPGHome.String()).To(Equal("/some/absolute/path")) -} - -func TestGnuPGHome_ApplyToMasterKey(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - - key := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(key) - g.Expect(key.gnuPGHomeDir).To(Equal(gnuPGHome.String())) - - gnuPGHome = "/non/existing/absolute/path/fails/validate" - gnuPGHome.ApplyToMasterKey(key) - g.Expect(key.gnuPGHomeDir).ToNot(Equal(gnuPGHome.String())) -} - -func TestMasterKey_Encrypt(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.ImportFile(mockPublicKey)).To(Succeed()) - - key := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(key) - data := []byte("oh no, my darkest secret") - g.Expect(key.Encrypt(data)).To(Succeed()) - - g.Expect(key.EncryptedKey).ToNot(BeEmpty()) - g.Expect(key.EncryptedKey).ToNot(Equal(data)) - - g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - - args := []string{ - "-d", - } - err, stdout, stderr := gpgExec(key.gnuPGHome(), args, strings.NewReader(key.EncryptedKey)) - g.Expect(err).ToNot(HaveOccurred(), stderr.String()) - g.Expect(stdout.Bytes()).To(Equal(data)) - - key.Fingerprint = "invalid" - err = key.Encrypt([]byte("invalid")) - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(Equal("failed to encrypt sops data key with pgp: gpg: 'invalid' is not a valid long keyID")) -} - -func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - - dataKey := []byte("foo") - - encryptKey := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(encryptKey) - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - t.Setenv("GNUPGHOME", gnuPGHome.String()) - decryptKey := pgp.NewMasterKeyFromFingerprint(mockFingerprint) - decryptKey.EncryptedKey = encryptKey.EncryptedKey - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptIfNeeded(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - - key := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(key) - g.Expect(key.EncryptIfNeeded([]byte("data"))).To(Succeed()) - - encryptedKey := key.EncryptedKey - g.Expect(encryptedKey).To(ContainSubstring("END PGP MESSAGE")) - - g.Expect(key.EncryptIfNeeded([]byte("some other data"))).To(Succeed()) - g.Expect(key.EncryptedKey).To(Equal(encryptedKey)) -} - -func TestMasterKey_EncryptedDataKey(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{EncryptedKey: "some key"} - g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) -} - -func TestMasterKey_Decrypt(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - - fingerprint := shortenFingerprint(mockFingerprint) - - data := []byte("this data is absolutely top secret") - err, stdout, stderr := gpgExec(gnuPGHome.String(), []string{ - "--no-default-recipient", - "--yes", - "--encrypt", - "-a", - "-r", - fingerprint, - "--trusted-key", - fingerprint, - "--no-encrypt-to", - }, bytes.NewReader(data)) - g.Expect(err).NotTo(HaveOccurred(), stderr.String()) - - encryptedData := stdout.String() - g.Expect(encryptedData).ToNot(BeEquivalentTo(data)) - - key := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(key) - key.EncryptedKey = encryptedData - - got, err := key.Decrypt() - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(got).To(Equal(data)) - - key.EncryptedKey = "absolute invalid" - got, err = key.Decrypt() - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("gpg: no valid OpenPGP data found")) - g.Expect(got).To(BeNil()) -} - -func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - - dataKey := []byte("foo") - - t.Setenv("GNUPGHOME", gnuPGHome.String()) - encryptKey := pgp.NewMasterKeyFromFingerprint(mockFingerprint) - g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) - - decryptKey := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(decryptKey) - decryptKey.EncryptedKey = encryptKey.EncryptedKey - - dec, err := decryptKey.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(dec).To(Equal(dataKey)) -} - -func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { - g := NewWithT(t) - - gnuPGHome, err := NewGnuPGHome() - g.Expect(err).ToNot(HaveOccurred()) - t.Cleanup(func() { - _ = os.RemoveAll(gnuPGHome.String()) - }) - g.Expect(gnuPGHome.ImportFile(mockPrivateKey)).To(Succeed()) - - key := MasterKeyFromFingerprint(mockFingerprint) - gnuPGHome.ApplyToMasterKey(key) - - data := []byte("some secret data") - g.Expect(key.Encrypt(data)).To(Succeed()) - g.Expect(key.EncryptedKey).ToNot(BeEmpty()) - - decryptedData, err := key.Decrypt() - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(decryptedData).To(Equal(data)) -} - -func TestMasterKey_NeedsRotation(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromFingerprint("") - g.Expect(key.NeedsRotation()).To(BeFalse()) - - key.CreationDate = key.CreationDate.Add(-(pgpTTL + time.Second)) - g.Expect(key.NeedsRotation()).To(BeTrue()) -} - -func TestMasterKey_ToString(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromFingerprint(mockFingerprint) - g.Expect(key.ToString()).To(Equal(mockFingerprint)) -} - -func TestMasterKey_ToMap(t *testing.T) { - g := NewWithT(t) - - key := MasterKeyFromFingerprint(mockFingerprint) - key.EncryptedKey = "data" - g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ - "fp": mockFingerprint, - "created_at": key.CreationDate.UTC().Format(time.RFC3339), - "enc": key.EncryptedKey, - })) -} - -func TestMasterKey_gnuPGHome(t *testing.T) { - g := NewWithT(t) - - key := &MasterKey{} - - usr, err := user.Current() - if err == nil { - g.Expect(key.gnuPGHome()).To(Equal(filepath.Join(usr.HomeDir, ".gnupg"))) - } else { - g.Expect(key.gnuPGHome()).To(Equal(filepath.Join(os.Getenv("HOME"), ".gnupg"))) - } - - gnupgHome := "/overwrite/home" - t.Setenv("GNUPGHOME", gnupgHome) - g.Expect(key.gnuPGHome()).To(Equal(gnupgHome)) - - key.gnuPGHomeDir = "/home/dir/overwrite" - g.Expect(key.gnuPGHome()).To(Equal(key.gnuPGHomeDir)) -} - -func Test_gpgBinary(t *testing.T) { - g := NewWithT(t) - - g.Expect(gpgBinary()).To(Equal("gpg")) - - overwrite := "/some/other/gpg" - t.Setenv(SopsGpgExecEnv, overwrite) - g.Expect(gpgBinary()).To(Equal(overwrite)) -} - -func Test_shortenFingerprint(t *testing.T) { - g := NewWithT(t) - - shortId := shortenFingerprint(mockFingerprint) - g.Expect(shortId).To(Equal("9732075EA221A7EA")) - - g.Expect(shortenFingerprint(shortId)).To(Equal(shortId)) -} - -func Fuzz_Pgp(f *testing.F) { - f.Fuzz(func(t *testing.T, seed, data []byte) { - fc := fuzz.NewConsumer(data) - masterKey := MasterKey{} - - if err := fc.GenerateStruct(&masterKey); err != nil { - return - } - - _ = masterKey.Encrypt(data) - _ = masterKey.EncryptIfNeeded(data) - }) -} diff --git a/internal/statusreaders/job.go b/internal/statusreaders/job.go deleted file mode 100644 index b78a70e68..000000000 --- a/internal/statusreaders/job.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package statusreaders - -import ( - "context" - "fmt" - - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" - kstatusreaders "sigs.k8s.io/cli-utils/pkg/kstatus/polling/statusreaders" - "sigs.k8s.io/cli-utils/pkg/kstatus/status" - "sigs.k8s.io/cli-utils/pkg/object" -) - -type customJobStatusReader struct { - genericStatusReader engine.StatusReader -} - -func NewCustomJobStatusReader(mapper meta.RESTMapper) engine.StatusReader { - genericStatusReader := kstatusreaders.NewGenericStatusReader(mapper, jobConditions) - return &customJobStatusReader{ - genericStatusReader: genericStatusReader, - } -} - -func (j *customJobStatusReader) Supports(gk schema.GroupKind) bool { - return gk == batchv1.SchemeGroupVersion.WithKind("Job").GroupKind() -} - -func (j *customJobStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) { - return j.genericStatusReader.ReadStatus(ctx, reader, resource) -} - -func (j *customJobStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) { - return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource) -} - -// Ref: https://github.com/kubernetes-sigs/cli-utils/blob/v0.29.4/pkg/kstatus/status/core.go -// Modified to return Current status only when the Job has completed as opposed to when it's in progress. -func jobConditions(u *unstructured.Unstructured) (*status.Result, error) { - obj := u.UnstructuredContent() - - parallelism := status.GetIntField(obj, ".spec.parallelism", 1) - completions := status.GetIntField(obj, ".spec.completions", parallelism) - succeeded := status.GetIntField(obj, ".status.succeeded", 0) - failed := status.GetIntField(obj, ".status.failed", 0) - - // Conditions - // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24 - objc, err := status.GetObjectWithConditions(obj) - if err != nil { - return nil, err - } - for _, c := range objc.Status.Conditions { - switch c.Type { - case "Complete": - if c.Status == corev1.ConditionTrue { - message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions) - return &status.Result{ - Status: status.CurrentStatus, - Message: message, - Conditions: []status.Condition{}, - }, nil - } - case "Failed": - message := fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions) - if c.Status == corev1.ConditionTrue { - return &status.Result{ - Status: status.FailedStatus, - Message: message, - Conditions: []status.Condition{ - { - Type: status.ConditionStalled, - Status: corev1.ConditionTrue, - Reason: "JobFailed", - Message: message, - }, - }, - }, nil - } - } - } - - message := "Job in progress" - return &status.Result{ - Status: status.InProgressStatus, - Message: message, - Conditions: []status.Condition{ - { - Type: status.ConditionReconciling, - Status: corev1.ConditionTrue, - Reason: "JobInProgress", - Message: message, - }, - }, - }, nil -} diff --git a/internal/statusreaders/job_test.go b/internal/statusreaders/job_test.go deleted file mode 100644 index 3a8ec65dd..000000000 --- a/internal/statusreaders/job_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2022 The Flux authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package statusreaders - -import ( - "testing" - - "github.com/fluxcd/pkg/runtime/patch" - . "github.com/onsi/gomega" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/cli-utils/pkg/kstatus/status" -) - -func Test_jobConditions(t *testing.T) { - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: "job", - }, - Spec: batchv1.JobSpec{}, - Status: batchv1.JobStatus{}, - } - - t.Run("job without Complete condition returns InProgress status", func(t *testing.T) { - g := NewWithT(t) - us, err := patch.ToUnstructured(job) - g.Expect(err).ToNot(HaveOccurred()) - result, err := jobConditions(us) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(result.Status).To(Equal(status.InProgressStatus)) - }) - - t.Run("job with Complete condition as True returns Current status", func(t *testing.T) { - g := NewWithT(t) - job.Status = batchv1.JobStatus{ - Conditions: []batchv1.JobCondition{ - { - Type: batchv1.JobComplete, - Status: corev1.ConditionTrue, - }, - }, - } - us, err := patch.ToUnstructured(job) - g.Expect(err).ToNot(HaveOccurred()) - result, err := jobConditions(us) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(result.Status).To(Equal(status.CurrentStatus)) - }) -} diff --git a/main.go b/main.go index c4272951c..773d9b1b6 100644 --- a/main.go +++ b/main.go @@ -27,26 +27,34 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/azure" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config" + ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/pkg/auth" + pkgcache "github.com/fluxcd/pkg/cache" "github.com/fluxcd/pkg/runtime/acl" runtimeClient "github.com/fluxcd/pkg/runtime/client" runtimeCtrl "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/events" feathelper "github.com/fluxcd/pkg/runtime/features" + "github.com/fluxcd/pkg/runtime/jitter" "github.com/fluxcd/pkg/runtime/leaderelection" "github.com/fluxcd/pkg/runtime/logger" + "github.com/fluxcd/pkg/runtime/metrics" "github.com/fluxcd/pkg/runtime/pprof" "github.com/fluxcd/pkg/runtime/probes" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - "github.com/fluxcd/kustomize-controller/controllers" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/kustomize-controller/internal/controller" "github.com/fluxcd/kustomize-controller/internal/features" - "github.com/fluxcd/kustomize-controller/internal/statusreaders" // +kubebuilder:scaffold:imports ) @@ -66,36 +74,44 @@ func init() { } func main() { + const ( + tokenCacheDefaultMaxSize = 100 + ) + var ( - metricsAddr string - eventsAddr string - healthAddr string - concurrent int - requeueDependency time.Duration - clientOptions runtimeClient.Options - kubeConfigOpts runtimeClient.KubeConfigOptions - logOptions logger.Options - leaderElectionOptions leaderelection.Options - rateLimiterOptions runtimeCtrl.RateLimiterOptions - aclOptions acl.Options - watchAllNamespaces bool - noRemoteBases bool - httpRetry int - defaultServiceAccount string - featureGates feathelper.FeatureGates + metricsAddr string + eventsAddr string + healthAddr string + concurrent int + concurrentSSA int + requeueDependency time.Duration + clientOptions runtimeClient.Options + kubeConfigOpts runtimeClient.KubeConfigOptions + logOptions logger.Options + leaderElectionOptions leaderelection.Options + rateLimiterOptions runtimeCtrl.RateLimiterOptions + watchOptions runtimeCtrl.WatchOptions + intervalJitterOptions jitter.IntervalOptions + aclOptions acl.Options + noRemoteBases bool + httpRetry int + defaultServiceAccount string + featureGates feathelper.FeatureGates + disallowedFieldManagers []string + tokenCacheOptions pkgcache.TokenFlags ) flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&eventsAddr, "events-addr", "", "The address of the events receiver.") flag.StringVar(&healthAddr, "health-addr", ":9440", "The address the health endpoint binds to.") flag.IntVar(&concurrent, "concurrent", 4, "The number of concurrent kustomize reconciles.") + flag.IntVar(&concurrentSSA, "concurrent-ssa", 4, "The number of concurrent server-side apply operations.") flag.DurationVar(&requeueDependency, "requeue-dependency", 30*time.Second, "The interval at which failing dependencies are reevaluated.") - flag.BoolVar(&watchAllNamespaces, "watch-all-namespaces", true, - "Watch for custom resources in all namespaces, if set to false it will only watch the runtime namespace.") flag.BoolVar(&noRemoteBases, "no-remote-bases", false, "Disallow remote bases usage in Kustomize overlays. When this flag is enabled, all resources must refer to local files included in the source artifact.") flag.IntVar(&httpRetry, "http-retry", 9, "The maximum number of retries when failing to fetch artifacts over HTTP.") flag.StringVar(&defaultServiceAccount, "default-service-account", "", "Default service account used for impersonation.") + flag.StringArrayVar(&disallowedFieldManagers, "override-manager", []string{}, "Field manager disallowed to perform changes on managed resources.") clientOptions.BindFlags(flag.CommandLine) logOptions.BindFlags(flag.CommandLine) @@ -104,21 +120,45 @@ func main() { kubeConfigOpts.BindFlags(flag.CommandLine) rateLimiterOptions.BindFlags(flag.CommandLine) featureGates.BindFlags(flag.CommandLine) + watchOptions.BindFlags(flag.CommandLine) + intervalJitterOptions.BindFlags(flag.CommandLine) + tokenCacheOptions.BindFlags(flag.CommandLine, tokenCacheDefaultMaxSize) flag.Parse() + logger.SetLogger(logger.NewLogger(logOptions)) + + ctx := ctrl.SetupSignalHandler() + if err := featureGates.WithLogger(setupLog).SupportedFeatures(features.FeatureGates()); err != nil { setupLog.Error(err, "unable to load feature gates") os.Exit(1) } - ctrl.SetLogger(logger.NewLogger(logOptions)) + switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); { + case err != nil: + setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity) + os.Exit(1) + case enabled: + auth.EnableObjectLevelWorkloadIdentity() + } + + if err := intervalJitterOptions.SetGlobalJitter(nil); err != nil { + setupLog.Error(err, "unable to set global jitter") + os.Exit(1) + } watchNamespace := "" - if !watchAllNamespaces { + if !watchOptions.AllNamespaces { watchNamespace = os.Getenv("RUNTIME_NAMESPACE") } + watchSelector, err := runtimeCtrl.GetWatchSelector(watchOptions) + if err != nil { + setupLog.Error(err, "unable to configure watch label selector for manager") + os.Exit(1) + } + var disableCacheFor []ctrlclient.Object shouldCache, err := features.Enabled(features.CacheSecretsAndConfigMaps) if err != nil { @@ -129,29 +169,55 @@ func main() { disableCacheFor = append(disableCacheFor, &corev1.Secret{}, &corev1.ConfigMap{}) } + leaderElectionId := fmt.Sprintf("%s-%s", controllerName, "leader-election") + if watchOptions.LabelSelector != "" { + leaderElectionId = leaderelection.GenerateID(leaderElectionId, watchOptions.LabelSelector) + } + restConfig := runtimeClient.GetConfigOrDie(clientOptions) - mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ + mgrConfig := ctrl.Options{ Scheme: scheme, - MetricsBindAddress: metricsAddr, HealthProbeBindAddress: healthAddr, - Port: 9443, LeaderElection: leaderElectionOptions.Enable, LeaderElectionReleaseOnCancel: leaderElectionOptions.ReleaseOnCancel, LeaseDuration: &leaderElectionOptions.LeaseDuration, RenewDeadline: &leaderElectionOptions.RenewDeadline, RetryPeriod: &leaderElectionOptions.RetryPeriod, - LeaderElectionID: fmt.Sprintf("%s-leader-election", controllerName), - Namespace: watchNamespace, + LeaderElectionID: leaderElectionId, Logger: ctrl.Log, - ClientDisableCacheFor: disableCacheFor, - }) + Client: ctrlclient.Options{ + Cache: &ctrlclient.CacheOptions{ + DisableFor: disableCacheFor, + }, + }, + Cache: ctrlcache.Options{ + ByObject: map[ctrlclient.Object]ctrlcache.ByObject{ + &kustomizev1.Kustomization{}: {Label: watchSelector}, + }, + }, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + ExtraHandlers: pprof.GetHandlers(), + }, + Controller: ctrlcfg.Controller{ + MaxConcurrentReconciles: concurrent, + RecoverPanic: ptr.To(true), + }, + } + + if watchNamespace != "" { + mgrConfig.Cache.DefaultNamespaces = map[string]ctrlcache.Config{ + watchNamespace: ctrlcache.Config{}, + } + } + + mgr, err := ctrl.NewManager(restConfig, mgrConfig) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } probes.SetupChecks(mgr, setupLog) - pprof.SetupHandlers(mgr, setupLog) var eventRecorder *events.Recorder if eventRecorder, err = events.NewRecorder(mgr, ctrl.Log, eventsAddr, controllerName); err != nil { @@ -159,25 +225,68 @@ func main() { os.Exit(1) } - metricsH := runtimeCtrl.MustMakeMetrics(mgr) - - jobStatusReader := statusreaders.NewCustomJobStatusReader(mgr.GetRESTMapper()) - pollingOpts := polling.Options{ - CustomStatusReaders: []engine.StatusReader{jobStatusReader}, - } - if err = (&controllers.KustomizationReconciler{ - ControllerName: controllerName, - DefaultServiceAccount: defaultServiceAccount, - Client: mgr.GetClient(), - Metrics: metricsH, - EventRecorder: eventRecorder, - NoCrossNamespaceRefs: aclOptions.NoCrossNamespaceRefs, - NoRemoteBases: noRemoteBases, - KubeConfigOpts: kubeConfigOpts, - PollingOpts: pollingOpts, - StatusPoller: polling.NewStatusPoller(mgr.GetClient(), mgr.GetRESTMapper(), pollingOpts), - }).SetupWithManager(mgr, controllers.KustomizationReconcilerOptions{ - MaxConcurrentReconciles: concurrent, + metricsH := runtimeCtrl.NewMetrics(mgr, metrics.MustMakeRecorder(), kustomizev1.KustomizationFinalizer) + + restMapper, err := runtimeClient.NewDynamicRESTMapper(mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "unable to create REST mapper") + os.Exit(1) + } + + var clusterReader engine.ClusterReaderFactory + if ok, _ := features.Enabled(features.DisableStatusPollerCache); ok { + clusterReader = engine.ClusterReaderFactoryFunc(clusterreader.NewDirectClusterReader) + } + + failFast := true + if ok, _ := features.Enabled(features.DisableFailFastBehavior); ok { + failFast = false + } + + strictSubstitutions, err := features.Enabled(features.StrictPostBuildSubstitutions) + if err != nil { + setupLog.Error(err, "unable to check feature gate "+features.StrictPostBuildSubstitutions) + os.Exit(1) + } + + groupChangeLog, err := features.Enabled(features.GroupChangeLog) + if err != nil { + setupLog.Error(err, "unable to check feature gate "+features.GroupChangeLog) + os.Exit(1) + } + + var tokenCache *pkgcache.TokenCache + if tokenCacheOptions.MaxSize > 0 { + var err error + tokenCache, err = pkgcache.NewTokenCache(tokenCacheOptions.MaxSize, + pkgcache.WithMaxDuration(tokenCacheOptions.MaxDuration), + pkgcache.WithMetricsRegisterer(ctrlmetrics.Registry), + pkgcache.WithMetricsPrefix("gotk_token_")) + if err != nil { + setupLog.Error(err, "unable to create token cache") + os.Exit(1) + } + } + + if err = (&controller.KustomizationReconciler{ + ControllerName: controllerName, + DefaultServiceAccount: defaultServiceAccount, + Client: mgr.GetClient(), + Mapper: restMapper, + APIReader: mgr.GetAPIReader(), + Metrics: metricsH, + EventRecorder: eventRecorder, + NoCrossNamespaceRefs: aclOptions.NoCrossNamespaceRefs, + NoRemoteBases: noRemoteBases, + FailFast: failFast, + ConcurrentSSA: concurrentSSA, + KubeConfigOpts: kubeConfigOpts, + ClusterReader: clusterReader, + DisallowedFieldManagers: disallowedFieldManagers, + StrictSubstitutions: strictSubstitutions, + GroupChangeLog: groupChangeLog, + TokenCache: tokenCache, + }).SetupWithManager(ctx, mgr, controller.KustomizationReconcilerOptions{ DependencyRequeueInterval: requeueDependency, HTTPRetry: httpRetry, RateLimiter: runtimeCtrl.GetRateLimiter(rateLimiterOptions), @@ -188,7 +297,7 @@ func main() { // +kubebuilder:scaffold:builder setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } diff --git a/tests/fuzz/Dockerfile.builder b/tests/fuzz/Dockerfile.builder index 059d3e696..75a0e08fb 100644 --- a/tests/fuzz/Dockerfile.builder +++ b/tests/fuzz/Dockerfile.builder @@ -1,5 +1,11 @@ FROM gcr.io/oss-fuzz-base/base-builder-go +RUN wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz \ + && mkdir temp-go \ + && rm -rf /root/.go/* \ + && tar -C temp-go/ -xzf go1.24.0.linux-amd64.tar.gz \ + && mv temp-go/go/* /root/.go/ + ENV SRC=$GOPATH/src/github.com/fluxcd/kustomize-controller ENV FLUX_CI=true diff --git a/tests/fuzz/oss_fuzz_prebuild.sh b/tests/fuzz/oss_fuzz_prebuild.sh index b0399af7a..b32efd0ae 100755 --- a/tests/fuzz/oss_fuzz_prebuild.sh +++ b/tests/fuzz/oss_fuzz_prebuild.sh @@ -21,5 +21,5 @@ set -euxo pipefail # Some tests requires embedded resources. Embedding does not allow # for traversing into ascending dirs, therefore we copy those contents here: -mkdir -p controllers/testdata/crd -cp config/crd/bases/*.yaml controllers/testdata/crd +mkdir -p internal/controller/testdata/crd +cp config/crd/bases/*.yaml internal/controller/testdata/crd