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:
-
-Package v1beta2 contains API Schema definitions for the kustomize.toolkit.fluxcd.io v1beta2 API group.
+
+Package v1 contains API Schema definitions for the kustomize.toolkit.fluxcd.io
+v1 API group.
Resource Types:
-Kustomization is the Schema for the kustomizations API.
-Decryption
+Decryption
(Appears on:
-KustomizationSpec )
+KustomizationSpec )
Decryption defines how decryption is handled for Kubernetes manifests.
-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.
-KustomizationStatus
+KustomizationStatus
(Appears on:
-Kustomization )
+Kustomization )
KustomizationStatus defines the observed state of a kustomization.
-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.
-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