Compare commits
51 Commits
rfc-creds
...
plugin-sys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fe0a48015 | ||
|
|
131cee951f | ||
|
|
1db4e66099 | ||
|
|
d9f51d047d | ||
|
|
dc5631f12b | ||
|
|
3f9d5bdc3d | ||
|
|
64e18014c3 | ||
|
|
e9226713e8 | ||
|
|
6a5e644798 | ||
|
|
0b0be7c1b6 | ||
|
|
484346ffcc | ||
|
|
5b3acbfcb5 | ||
|
|
2288dd90d6 | ||
|
|
af05357a62 | ||
|
|
64808a0eac | ||
|
|
2ead4fb31c | ||
|
|
b60dfbe970 | ||
|
|
ee8bb8d8a0 | ||
|
|
5f3098477e | ||
|
|
4c79a76e94 | ||
|
|
1516761fc8 | ||
|
|
52b1c1152b | ||
|
|
ab4bbffa5b | ||
|
|
e7314e8926 | ||
|
|
2666eaf8fc | ||
|
|
8262f8099e | ||
|
|
cbc5c736f4 | ||
|
|
ac71dd88a3 | ||
|
|
c5e5dfb8ae | ||
|
|
6ae880501e | ||
|
|
fd547dfe42 | ||
|
|
436dc7920a | ||
|
|
7a8cf63623 | ||
|
|
a6aefab55b | ||
|
|
5e5ee73046 | ||
|
|
8362c88791 | ||
|
|
340a048e8b | ||
|
|
4b2fc84402 | ||
|
|
2673348c2f | ||
|
|
7132eb3435 | ||
|
|
473b02ce5c | ||
|
|
862d9ddb6d | ||
|
|
33b9345883 | ||
|
|
e169a97577 | ||
|
|
4eddf80724 | ||
|
|
99f182be06 | ||
|
|
cf785cebcc | ||
|
|
7ff4c32d16 | ||
|
|
75bf2d608f | ||
|
|
f950198f9d | ||
|
|
2a2201fe56 |
6
.github/labels.yaml
vendored
6
.github/labels.yaml
vendored
@@ -44,12 +44,12 @@
|
||||
description: Feature request proposals in the RFC format
|
||||
color: '#D621C3'
|
||||
aliases: ['area/RFC']
|
||||
- name: backport:release/v2.5.x
|
||||
description: To be backported to release/v2.5.x
|
||||
color: '#ffd700'
|
||||
- name: backport:release/v2.6.x
|
||||
description: To be backported to release/v2.6.x
|
||||
color: '#ffd700'
|
||||
- name: backport:release/v2.7.x
|
||||
description: To be backported to release/v2.7.x
|
||||
color: '#ffd700'
|
||||
- name: backport:release/v2.8.x
|
||||
description: To be backported to release/v2.8.x
|
||||
color: '#ffd700'
|
||||
|
||||
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -23,7 +23,7 @@ amd when it finds a new controller version, the workflow performs the following
|
||||
- Updates the controller API package version in `go.mod`.
|
||||
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
||||
- Patches the controller Deployment version in `manifests/bases` overlay.
|
||||
- Opens a Pull Request against the `main` branch.
|
||||
- Opens a Pull Request against the checked out branch.
|
||||
- Triggers the e2e test suite to run for the opened PR.
|
||||
|
||||
|
||||
|
||||
2
.github/workflows/action.yaml
vendored
2
.github/workflows/action.yaml
vendored
@@ -24,6 +24,6 @@ jobs:
|
||||
name: action on ${{ matrix.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup flux
|
||||
uses: ./action
|
||||
|
||||
2
.github/workflows/backport.yaml
vendored
2
.github/workflows/backport.yaml
vendored
@@ -8,6 +8,6 @@ jobs:
|
||||
permissions:
|
||||
contents: write # for reading and creating branches.
|
||||
pull-requests: write # for creating pull requests against release branches.
|
||||
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.4.0
|
||||
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
|
||||
34
.github/workflows/conformance.yaml
vendored
34
.github/workflows/conformance.yaml
vendored
@@ -3,13 +3,13 @@ name: conformance
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
|
||||
branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.25.x
|
||||
GO_VERSION: 1.26.x
|
||||
|
||||
jobs:
|
||||
conform-kubernetes:
|
||||
@@ -23,9 +23,9 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
make build
|
||||
- name: Setup Kubernetes
|
||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
with:
|
||||
version: v0.30.0
|
||||
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
||||
@@ -76,13 +76,13 @@ jobs:
|
||||
matrix:
|
||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||
# Available versions can be found with "replicated cluster versions"
|
||||
K3S_VERSION: [ 1.32.9, 1.33.5, 1.34.1 ]
|
||||
K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: |
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Build
|
||||
run: make build-dev
|
||||
- name: Create repository
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||
- name: Create cluster
|
||||
id: create-cluster
|
||||
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
kubernetes-distribution: "k3s"
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
kubectl delete ns flux-system --wait
|
||||
- name: Delete cluster
|
||||
if: ${{ always() }}
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
@@ -168,13 +168,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
||||
OPENSHIFT_VERSION: [ 4.19.0-okd, 4.20.0-okd ]
|
||||
OPENSHIFT_VERSION: [ 4.20.0-okd ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: |
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Build
|
||||
run: make build-dev
|
||||
- name: Create repository
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||
- name: Create cluster
|
||||
id: create-cluster
|
||||
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
kubernetes-distribution: "openshift"
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
kubectl delete ns flux-system --wait
|
||||
- name: Delete cluster
|
||||
if: ${{ always() }}
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
|
||||
8
.github/workflows/e2e-azure.yaml
vendored
8
.github/workflows/e2e-azure.yaml
vendored
@@ -29,14 +29,14 @@ jobs:
|
||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: CheckoutD
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
cache-dependency-path: tests/integration/go.sum
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
||||
- name: Setup Flux CLI
|
||||
run: make build
|
||||
working-directory: ./
|
||||
|
||||
16
.github/workflows/e2e-bootstrap.yaml
vendored
16
.github/workflows/e2e-bootstrap.yaml
vendored
@@ -17,27 +17,27 @@ jobs:
|
||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Setup Kubernetes
|
||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
with:
|
||||
version: v0.30.0
|
||||
cluster_name: kind
|
||||
# The versions below should target the newest Kubernetes version
|
||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
|
||||
kubectl_version: v1.32.0
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||
kubectl_version: v1.33.0
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Setup yq
|
||||
uses: fluxcd/pkg/actions/yq@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/yq@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Build
|
||||
run: make build-dev
|
||||
- name: Set outputs
|
||||
|
||||
14
.github/workflows/e2e-gcp.yaml
vendored
14
.github/workflows/e2e-gcp.yaml
vendored
@@ -29,14 +29,14 @@ jobs:
|
||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
cache-dependency-path: tests/integration/go.sum
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
||||
- name: Setup Flux CLI
|
||||
run: make build
|
||||
working-directory: ./
|
||||
@@ -56,11 +56,11 @@ jobs:
|
||||
- name: Setup gcloud
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- name: Log into us-central1-docker.pkg.dev
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: us-central1-docker.pkg.dev
|
||||
username: oauth2accesstoken
|
||||
|
||||
14
.github/workflows/e2e.yaml
vendored
14
.github/workflows/e2e.yaml
vendored
@@ -23,16 +23,16 @@ jobs:
|
||||
- 5000:5000
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Setup Kubernetes
|
||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
with:
|
||||
version: v0.30.0
|
||||
cluster_name: kind
|
||||
@@ -40,13 +40,13 @@ jobs:
|
||||
config: .github/kind/config.yaml # disable KIND-net
|
||||
# The versions below should target the oldest supported Kubernetes version
|
||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
|
||||
kubectl_version: v1.32.0
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||
kubectl_version: v1.33.0
|
||||
- name: Setup Calico for network policy
|
||||
run: |
|
||||
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- name: Run e2e tests
|
||||
|
||||
6
.github/workflows/ossf.yaml
vendored
6
.github/workflows/ossf.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
@@ -28,12 +28,12 @@ jobs:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_results: true
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
- name: Upload SARIF results
|
||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
34
.github/workflows/release.yaml
vendored
34
.github/workflows/release.yaml
vendored
@@ -22,35 +22,35 @@ jobs:
|
||||
packages: write # needed for ghcr access
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Unshallow
|
||||
run: git fetch --prune --unshallow
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
cache: false
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
- name: Setup Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- name: Setup Syft
|
||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
|
||||
uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
|
||||
- name: Setup Cosign
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
with:
|
||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
run: |
|
||||
kustomize build manifests/crds > all-crds.yaml
|
||||
- name: Generate OpenAPI JSON schemas from CRDs
|
||||
uses: fluxcd/pkg/actions/crdjsonschema@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/crdjsonschema@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
with:
|
||||
crd: all-crds.yaml
|
||||
output: schemas
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
||||
- name: Run GoReleaser
|
||||
id: run-goreleaser
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip=validate
|
||||
@@ -103,9 +103,9 @@ jobs:
|
||||
id-token: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
- name: Setup Flux CLI
|
||||
uses: ./action/
|
||||
with:
|
||||
@@ -116,13 +116,13 @@ jobs:
|
||||
VERSION=$(flux version --client | awk '{ print $NF }')
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
--path="./flux-system" \
|
||||
--source=${{ github.repositoryUrl }} \
|
||||
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
||||
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
- uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
with:
|
||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||
- name: Sign manifests
|
||||
|
||||
2
.github/workflows/scan.yaml
vendored
2
.github/workflows/scan.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read # for reading the repository code.
|
||||
security-events: write # for uploading the CodeQL analysis results.
|
||||
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.4.0
|
||||
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
||||
|
||||
2
.github/workflows/sync-labels.yaml
vendored
2
.github/workflows/sync-labels.yaml
vendored
@@ -12,6 +12,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read # for reading the labels file.
|
||||
issues: write # for creating and updating labels.
|
||||
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.4.0
|
||||
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
10
.github/workflows/update.yaml
vendored
10
.github/workflows/update.yaml
vendored
@@ -16,11 +16,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
||||
signoff: true
|
||||
branch: update-components
|
||||
branch: update-components-${{ github.ref_name }}
|
||||
title: Update toolkit components
|
||||
body: |
|
||||
${{ steps.update.outputs.pr_body }}
|
||||
|
||||
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
Normal file
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: upgrade-fluxcd-pkg
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upgrade-fluxcd-pkg:
|
||||
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
@@ -68,8 +68,8 @@ for source changes.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
* go >= 1.25
|
||||
* kubectl >= 1.30
|
||||
* go >= 1.26
|
||||
* kubectl >= 1.33
|
||||
* kustomize >= 5.0
|
||||
|
||||
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.22 AS builder
|
||||
FROM alpine:3.23 AS builder
|
||||
|
||||
RUN apk add --no-cache ca-certificates curl
|
||||
|
||||
@@ -10,7 +10,7 @@ RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
|
||||
|
||||
RUN kubectl version --client=true
|
||||
|
||||
FROM alpine:3.22 AS flux-cli
|
||||
FROM alpine:3.23 AS flux-cli
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
|
||||
all: test build
|
||||
|
||||
tidy:
|
||||
go mod tidy -compat=1.25
|
||||
cd tests/integration && go mod tidy -compat=1.25
|
||||
go mod tidy -compat=1.26
|
||||
cd tests/integration && go mod tidy -compat=1.26
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
@@ -60,7 +60,7 @@ type checkFlags struct {
|
||||
}
|
||||
|
||||
var kubernetesConstraints = []string{
|
||||
">=1.32.0-0",
|
||||
">=1.33.0-0",
|
||||
}
|
||||
|
||||
var checkArgs checkFlags
|
||||
|
||||
@@ -196,11 +196,14 @@ func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []cli
|
||||
|
||||
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
||||
listOpts := &metav1.ListOptions{}
|
||||
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||
err := runtimeresource.FollowContinue(listOpts,
|
||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
newEvents := &corev1.EventList{}
|
||||
if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
|
||||
opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||
if options.Continue != "" {
|
||||
opts = append(opts, client.Continue(options.Continue))
|
||||
}
|
||||
if err := kubeclient.List(ctx, newEvents, opts...); err != nil {
|
||||
return nil, fmt.Errorf("error getting events: %w", err)
|
||||
}
|
||||
el.Items = append(el.Items, newEvents.Items...)
|
||||
|
||||
@@ -20,11 +20,13 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -419,6 +421,108 @@ func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event
|
||||
}
|
||||
}
|
||||
|
||||
// paginatedClient wraps a client.Client and simulates real Kubernetes API
|
||||
// pagination by splitting List results into pages of pageSize items,
|
||||
// using the ListMeta.Continue token.
|
||||
type paginatedClient struct {
|
||||
client.Client
|
||||
pageSize int
|
||||
}
|
||||
|
||||
func (c *paginatedClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||
listOpts := &client.ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
|
||||
// Fetch all results from the underlying client (without Limit/Continue).
|
||||
stripped := make([]client.ListOption, 0, len(opts))
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(client.Limit); ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := o.(client.Continue); ok {
|
||||
continue
|
||||
}
|
||||
stripped = append(stripped, o)
|
||||
}
|
||||
if err := c.Client.List(ctx, list, stripped...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items, err := meta.ExtractList(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine the page window based on the Continue token.
|
||||
start := 0
|
||||
if listOpts.Continue != "" {
|
||||
n, err := strconv.Atoi(listOpts.Continue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid continue token: %w", err)
|
||||
}
|
||||
start = n
|
||||
}
|
||||
if start > len(items) {
|
||||
start = len(items)
|
||||
}
|
||||
|
||||
end := start + c.pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
|
||||
page := items[start:end]
|
||||
if err := meta.SetList(list, page); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the Continue token when there are more pages.
|
||||
listAccessor, err := meta.ListAccessor(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if end < len(items) {
|
||||
listAccessor.SetContinue(strconv.Itoa(end))
|
||||
} else {
|
||||
listAccessor.SetContinue("")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_addEventsToList_pagination(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
objs, err := ssautil.ReadObjects(strings.NewReader(objects))
|
||||
g.Expect(err).To(Not(HaveOccurred()))
|
||||
|
||||
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
|
||||
for _, obj := range objs {
|
||||
builder = builder.WithObjects(obj)
|
||||
}
|
||||
|
||||
eventList := &corev1.EventList{}
|
||||
for _, obj := range objs {
|
||||
infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason")
|
||||
warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason")
|
||||
eventList.Items = append(eventList.Items, infoEvent, warningEvent)
|
||||
}
|
||||
builder = builder.WithLists(eventList)
|
||||
c := builder.Build()
|
||||
|
||||
totalEvents := len(eventList.Items)
|
||||
g.Expect(totalEvents).To(BeNumerically(">", 2), "need more than 2 events to test pagination")
|
||||
|
||||
// Wrap the client to paginate at 2 items per page, forcing multiple
|
||||
// round-trips through FollowContinue.
|
||||
pc := &paginatedClient{Client: c, pageSize: 2}
|
||||
|
||||
el := &corev1.EventList{}
|
||||
err = addEventsToList(context.Background(), pc, el, nil)
|
||||
g.Expect(err).To(Not(HaveOccurred()))
|
||||
g.Expect(el.Items).To(HaveLen(totalEvents),
|
||||
"addEventsToList should collect all events across paginated responses")
|
||||
}
|
||||
|
||||
func kindNameIndexer(obj client.Object) []string {
|
||||
e, ok := obj.(*corev1.Event)
|
||||
if !ok {
|
||||
|
||||
@@ -186,6 +186,8 @@ func main() {
|
||||
// logger, we configure it's logger to do nothing.
|
||||
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
||||
|
||||
registerPlugins()
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
||||
if err, ok := err.(*RequestError); ok {
|
||||
|
||||
@@ -59,71 +59,82 @@ type APIVersions struct {
|
||||
|
||||
// TODO: Update this mapping when new Flux minor versions are released!
|
||||
// latestAPIVersions contains the latest API versions for each GroupKind
|
||||
// for each supported Flux version. We maintain the latest two minor versions.
|
||||
// for each supported Flux version. The number of latest minor versions
|
||||
// we maintain here must match what's documented here:
|
||||
//
|
||||
// https://fluxcd.io/flux/releases/#supported-releases
|
||||
var latestAPIVersions = []APIVersions{
|
||||
{
|
||||
FluxVersion: "2.7",
|
||||
LatestVersions: map[schema.GroupKind]string{
|
||||
// source-controller
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||
|
||||
// kustomize-controller
|
||||
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||
|
||||
// helm-controller
|
||||
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||
|
||||
// notification-controller
|
||||
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||
|
||||
// image-reflector-controller
|
||||
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImageRepositoryKind}: imagev1.GroupVersion.Version,
|
||||
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImagePolicyKind}: imagev1.GroupVersion.Version,
|
||||
|
||||
// image-automation-controller
|
||||
{Group: imageautov1.GroupVersion.Group, Kind: imageautov1.ImageUpdateAutomationKind}: imageautov1.GroupVersion.Version,
|
||||
|
||||
// source-watcher
|
||||
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
||||
},
|
||||
FluxVersion: "2.8",
|
||||
LatestVersions: flux27LatestAPIVersions,
|
||||
},
|
||||
{
|
||||
FluxVersion: "2.6",
|
||||
LatestVersions: map[schema.GroupKind]string{
|
||||
// source-controller
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||
|
||||
// kustomize-controller
|
||||
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||
|
||||
// helm-controller
|
||||
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||
|
||||
// notification-controller
|
||||
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||
|
||||
// image-reflector-controller
|
||||
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImageRepositoryKind}: imagev1b2.GroupVersion.Version,
|
||||
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImagePolicyKind}: imagev1b2.GroupVersion.Version,
|
||||
|
||||
// image-automation-controller
|
||||
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
||||
},
|
||||
FluxVersion: "2.7",
|
||||
LatestVersions: flux27LatestAPIVersions,
|
||||
},
|
||||
{
|
||||
FluxVersion: "2.6",
|
||||
LatestVersions: flux26LatestAPIVersions,
|
||||
},
|
||||
}
|
||||
|
||||
var flux27LatestAPIVersions = map[schema.GroupKind]string{
|
||||
// source-controller
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||
|
||||
// kustomize-controller
|
||||
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||
|
||||
// helm-controller
|
||||
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||
|
||||
// notification-controller
|
||||
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||
|
||||
// image-reflector-controller
|
||||
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImageRepositoryKind}: imagev1.GroupVersion.Version,
|
||||
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImagePolicyKind}: imagev1.GroupVersion.Version,
|
||||
|
||||
// image-automation-controller
|
||||
{Group: imageautov1.GroupVersion.Group, Kind: imageautov1.ImageUpdateAutomationKind}: imageautov1.GroupVersion.Version,
|
||||
|
||||
// source-watcher
|
||||
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
||||
}
|
||||
|
||||
var flux26LatestAPIVersions = map[schema.GroupKind]string{
|
||||
// source-controller
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||
|
||||
// kustomize-controller
|
||||
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||
|
||||
// helm-controller
|
||||
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||
|
||||
// notification-controller
|
||||
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||
|
||||
// image-reflector-controller
|
||||
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImageRepositoryKind}: imagev1b2.GroupVersion.Version,
|
||||
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImagePolicyKind}: imagev1b2.GroupVersion.Version,
|
||||
|
||||
// image-automation-controller
|
||||
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
||||
}
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
|
||||
340
cmd/flux/plugin.go
Normal file
340
cmd/flux/plugin.go
Normal file
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
Copyright 2026 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||
)
|
||||
|
||||
var pluginHandler = plugin.NewHandler()
|
||||
|
||||
var pluginCmd = &cobra.Command{
|
||||
Use: "plugin",
|
||||
Short: "Manage Flux CLI plugins",
|
||||
Long: `The plugin sub-commands manage Flux CLI plugins.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// No-op: skip root's namespace DNS validation for plugin commands.
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var pluginListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List installed plugins",
|
||||
Long: `The plugin list command shows all installed plugins with their versions and paths.`,
|
||||
RunE: pluginListCmdRun,
|
||||
}
|
||||
|
||||
var pluginInstallCmd = &cobra.Command{
|
||||
Use: "install <name>[@<version>]",
|
||||
Short: "Install a plugin from the catalog",
|
||||
Long: `The plugin install command downloads and installs a plugin from the Flux plugin catalog.
|
||||
|
||||
Examples:
|
||||
# Install the latest version
|
||||
flux plugin install operator
|
||||
|
||||
# Install a specific version
|
||||
flux plugin install operator@0.45.0`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: pluginInstallCmdRun,
|
||||
}
|
||||
|
||||
var pluginUninstallCmd = &cobra.Command{
|
||||
Use: "uninstall <name>",
|
||||
Short: "Uninstall a plugin",
|
||||
Long: `The plugin uninstall command removes a plugin binary and its receipt from the plugin directory.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: pluginUninstallCmdRun,
|
||||
}
|
||||
|
||||
var pluginUpdateCmd = &cobra.Command{
|
||||
Use: "update [name]",
|
||||
Short: "Update installed plugins",
|
||||
Long: `The plugin update command updates installed plugins to their latest versions.
|
||||
|
||||
Examples:
|
||||
# Update a single plugin
|
||||
flux plugin update operator
|
||||
|
||||
# Update all installed plugins
|
||||
flux plugin update`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: pluginUpdateCmdRun,
|
||||
}
|
||||
|
||||
var pluginSearchCmd = &cobra.Command{
|
||||
Use: "search [query]",
|
||||
Short: "Search the plugin catalog",
|
||||
Long: `The plugin search command lists available plugins from the Flux plugin catalog.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: pluginSearchCmdRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
pluginCmd.AddCommand(pluginListCmd)
|
||||
pluginCmd.AddCommand(pluginInstallCmd)
|
||||
pluginCmd.AddCommand(pluginUninstallCmd)
|
||||
pluginCmd.AddCommand(pluginUpdateCmd)
|
||||
pluginCmd.AddCommand(pluginSearchCmd)
|
||||
rootCmd.AddCommand(pluginCmd)
|
||||
}
|
||||
|
||||
// builtinCommandNames returns the names of all non-plugin commands on rootCmd.
|
||||
func builtinCommandNames() []string {
|
||||
var names []string
|
||||
for _, c := range rootCmd.Commands() {
|
||||
if c.GroupID != "plugin" {
|
||||
names = append(names, c.Name())
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// registerPlugins scans the plugin directory and registers discovered
|
||||
// plugins as Cobra subcommands on rootCmd.
|
||||
func registerPlugins() {
|
||||
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||
if len(plugins) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !rootCmd.ContainsGroup("plugin") {
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "plugin",
|
||||
Title: "Plugin Commands:",
|
||||
})
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
cmd := &cobra.Command{
|
||||
Use: p.Name,
|
||||
Short: fmt.Sprintf("Runs the %s plugin", p.Name),
|
||||
Long: fmt.Sprintf("This command runs the %s plugin.\nUse 'flux %s --help' for full plugin help.", p.Name, p.Name),
|
||||
DisableFlagParsing: true,
|
||||
GroupID: "plugin",
|
||||
ValidArgsFunction: plugin.CompleteFunc(p.Path),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return plugin.Exec(p.Path, args)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func pluginListCmdRun(cmd *cobra.Command, args []string) error {
|
||||
pluginDir := pluginHandler.PluginDir()
|
||||
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||
if len(plugins) == 0 {
|
||||
cmd.Println("No plugins found")
|
||||
return nil
|
||||
}
|
||||
|
||||
header := []string{"NAME", "VERSION", "PATH"}
|
||||
var rows [][]string
|
||||
for _, p := range plugins {
|
||||
version := "manual"
|
||||
if receipt := plugin.ReadReceipt(pluginDir, p.Name); receipt != nil {
|
||||
version = receipt.Version
|
||||
}
|
||||
rows = append(rows, []string{p.Name, version, p.Path})
|
||||
}
|
||||
|
||||
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||
}
|
||||
|
||||
func pluginInstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||
nameVersion := args[0]
|
||||
name, version := parseNameVersion(nameVersion)
|
||||
|
||||
catalogClient := newCatalogClient()
|
||||
manifest, err := catalogClient.FetchManifest(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pv, err := plugin.ResolveVersion(manifest, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plat, err := plugin.ResolvePlatform(pv, runtime.GOOS, runtime.GOARCH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plugin %q v%s has no binary for %s/%s", name, pv.Version, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
pluginDir := pluginHandler.EnsurePluginDir()
|
||||
|
||||
installer := plugin.NewInstaller()
|
||||
sp := newPluginSpinner(fmt.Sprintf("installing %s v%s", name, pv.Version))
|
||||
sp.Start()
|
||||
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||
sp.Stop()
|
||||
return err
|
||||
}
|
||||
sp.Stop()
|
||||
|
||||
logger.Successf("installed %s v%s", name, pv.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginUninstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
pluginDir := pluginHandler.PluginDir()
|
||||
|
||||
if err := plugin.Uninstall(pluginDir, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Successf("uninstalled %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginUpdateCmdRun(cmd *cobra.Command, args []string) error {
|
||||
catalogClient := newCatalogClient()
|
||||
|
||||
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||
if len(plugins) == 0 {
|
||||
cmd.Println("No plugins found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a specific plugin is requested, filter to just that one.
|
||||
if len(args) == 1 {
|
||||
name := args[0]
|
||||
var found bool
|
||||
for _, p := range plugins {
|
||||
if p.Name == name {
|
||||
plugins = []plugin.Plugin{p}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("plugin %q is not installed", name)
|
||||
}
|
||||
}
|
||||
|
||||
pluginDir := pluginHandler.EnsurePluginDir()
|
||||
installer := plugin.NewInstaller()
|
||||
for _, p := range plugins {
|
||||
result := plugin.CheckUpdate(pluginDir, p.Name, catalogClient, runtime.GOOS, runtime.GOARCH)
|
||||
if result.Err != nil {
|
||||
logger.Failuref("error checking %s: %v", p.Name, result.Err)
|
||||
continue
|
||||
}
|
||||
if result.Skipped {
|
||||
if result.SkipReason == plugin.SkipReasonManual {
|
||||
logger.Warningf("skipping %s (%s)", p.Name, result.SkipReason)
|
||||
} else {
|
||||
logger.Successf("%s already up to date (v%s)", p.Name, result.FromVersion)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
sp := newPluginSpinner(fmt.Sprintf("updating %s v%s → v%s", p.Name, result.FromVersion, result.ToVersion))
|
||||
sp.Start()
|
||||
if err := installer.Install(pluginDir, result.Manifest, result.Version, result.Platform); err != nil {
|
||||
sp.Stop()
|
||||
logger.Failuref("error updating %s: %v", p.Name, err)
|
||||
continue
|
||||
}
|
||||
sp.Stop()
|
||||
logger.Successf("updated %s v%s → v%s", p.Name, result.FromVersion, result.ToVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginSearchCmdRun(cmd *cobra.Command, args []string) error {
|
||||
catalogClient := newCatalogClient()
|
||||
catalog, err := catalogClient.FetchCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var query string
|
||||
if len(args) == 1 {
|
||||
query = strings.ToLower(args[0])
|
||||
}
|
||||
|
||||
pluginDir := pluginHandler.PluginDir()
|
||||
header := []string{"NAME", "DESCRIPTION", "INSTALLED"}
|
||||
var rows [][]string
|
||||
for _, entry := range catalog.Plugins {
|
||||
if query != "" {
|
||||
if !strings.Contains(strings.ToLower(entry.Name), query) &&
|
||||
!strings.Contains(strings.ToLower(entry.Description), query) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
installed := ""
|
||||
if receipt := plugin.ReadReceipt(pluginDir, entry.Name); receipt != nil {
|
||||
installed = receipt.Version
|
||||
}
|
||||
|
||||
rows = append(rows, []string{entry.Name, entry.Description, installed})
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
if query != "" {
|
||||
cmd.Printf("No plugins matching %q found in catalog\n", query)
|
||||
} else {
|
||||
cmd.Println("No plugins found in catalog")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||
}
|
||||
|
||||
// parseNameVersion splits "operator@0.45.0" into ("operator", "0.45.0").
|
||||
// If no @ is present, version is empty (latest).
|
||||
func parseNameVersion(s string) (string, string) {
|
||||
name, version, found := strings.Cut(s, "@")
|
||||
if found {
|
||||
return name, version
|
||||
}
|
||||
return s, ""
|
||||
}
|
||||
|
||||
// newCatalogClient creates a CatalogClient that respects FLUXCD_PLUGIN_CATALOG.
|
||||
func newCatalogClient() *plugin.CatalogClient {
|
||||
client := plugin.NewCatalogClient()
|
||||
client.GetEnv = pluginHandler.GetEnv
|
||||
return client
|
||||
}
|
||||
|
||||
func newPluginSpinner(message string) *spinner.Spinner {
|
||||
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||
s.Suffix = " " + message
|
||||
return s
|
||||
}
|
||||
265
cmd/flux/plugin_test.go
Normal file
265
cmd/flux/plugin_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
Copyright 2026 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||
)
|
||||
|
||||
func TestPluginAppearsInHelp(t *testing.T) {
|
||||
origHandler := pluginHandler
|
||||
defer func() { pluginHandler = origHandler }()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
fakeBin := pluginDir + "/flux-testplugin"
|
||||
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||
|
||||
pluginHandler = &plugin.Handler{
|
||||
ReadDir: os.ReadDir,
|
||||
Stat: os.Stat,
|
||||
GetEnv: func(key string) string {
|
||||
if key == "FLUXCD_PLUGINS" {
|
||||
return pluginDir
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||
}
|
||||
|
||||
registerPlugins()
|
||||
defer func() {
|
||||
cmds := rootCmd.Commands()
|
||||
for _, cmd := range cmds {
|
||||
if cmd.Name() == "testplugin" {
|
||||
rootCmd.RemoveCommand(cmd)
|
||||
break
|
||||
}
|
||||
}
|
||||
rootCmd.SetHelpTemplate("")
|
||||
}()
|
||||
|
||||
output, err := executeCommand("--help")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "Plugin Commands:") {
|
||||
t.Error("expected 'Plugin Commands:' in help output")
|
||||
}
|
||||
if !strings.Contains(output, "testplugin") {
|
||||
t.Error("expected 'testplugin' in help output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginListOutput(t *testing.T) {
|
||||
origHandler := pluginHandler
|
||||
defer func() { pluginHandler = origHandler }()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
fakeBin := pluginDir + "/flux-myplugin"
|
||||
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||
|
||||
pluginHandler = &plugin.Handler{
|
||||
ReadDir: os.ReadDir,
|
||||
Stat: os.Stat,
|
||||
GetEnv: func(key string) string {
|
||||
if key == "FLUXCD_PLUGINS" {
|
||||
return pluginDir
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||
}
|
||||
|
||||
output, err := executeCommand("plugin list")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "myplugin") {
|
||||
t.Errorf("expected 'myplugin' in output, got: %s", output)
|
||||
}
|
||||
if !strings.Contains(output, "manual") {
|
||||
t.Errorf("expected 'manual' in output (no receipt), got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginListWithReceipt(t *testing.T) {
|
||||
origHandler := pluginHandler
|
||||
defer func() { pluginHandler = origHandler }()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
fakeBin := pluginDir + "/flux-myplugin"
|
||||
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||
receipt := pluginDir + "/flux-myplugin.yaml"
|
||||
os.WriteFile(receipt, []byte("name: myplugin\nversion: \"1.2.3\"\n"), 0o644)
|
||||
|
||||
pluginHandler = &plugin.Handler{
|
||||
ReadDir: os.ReadDir,
|
||||
Stat: os.Stat,
|
||||
GetEnv: func(key string) string {
|
||||
if key == "FLUXCD_PLUGINS" {
|
||||
return pluginDir
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||
}
|
||||
|
||||
output, err := executeCommand("plugin list")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "1.2.3") {
|
||||
t.Errorf("expected version '1.2.3' in output, got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginListEmpty(t *testing.T) {
|
||||
origHandler := pluginHandler
|
||||
defer func() { pluginHandler = origHandler }()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
pluginHandler = &plugin.Handler{
|
||||
ReadDir: os.ReadDir,
|
||||
Stat: os.Stat,
|
||||
GetEnv: func(key string) string {
|
||||
if key == "FLUXCD_PLUGINS" {
|
||||
return pluginDir
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||
}
|
||||
|
||||
output, err := executeCommand("plugin list")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "No plugins found") {
|
||||
t.Errorf("expected 'No plugins found', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoPluginsNoRegistration(t *testing.T) {
|
||||
origHandler := pluginHandler
|
||||
defer func() { pluginHandler = origHandler }()
|
||||
|
||||
pluginHandler = &plugin.Handler{
|
||||
ReadDir: func(name string) ([]os.DirEntry, error) {
|
||||
return nil, fmt.Errorf("no dir")
|
||||
},
|
||||
Stat: os.Stat,
|
||||
GetEnv: func(key string) string {
|
||||
if key == "FLUXCD_PLUGINS" {
|
||||
return "/nonexistent"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||
}
|
||||
|
||||
// Verify that registerPlugins with no plugins doesn't add any commands.
|
||||
before := len(rootCmd.Commands())
|
||||
registerPlugins()
|
||||
after := len(rootCmd.Commands())
|
||||
if after != before {
|
||||
t.Errorf("expected no new commands, got %d new", after-before)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginSkipsPersistentPreRun(t *testing.T) {
|
||||
// Plugin commands override root's PersistentPreRunE with a no-op,
|
||||
// so an invalid namespace should not trigger a validation error.
|
||||
_, err := executeCommand("plugin list")
|
||||
if err != nil {
|
||||
t.Fatalf("plugin list should not trigger root's namespace validation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNameVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantName string
|
||||
wantVersion string
|
||||
}{
|
||||
{"operator", "operator", ""},
|
||||
{"operator@0.45.0", "operator", "0.45.0"},
|
||||
{"my-tool@1.0.0", "my-tool", "1.0.0"},
|
||||
{"plugin@", "plugin", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
name, version := parseNameVersion(tt.input)
|
||||
if name != tt.wantName {
|
||||
t.Errorf("name: got %q, want %q", name, tt.wantName)
|
||||
}
|
||||
if version != tt.wantVersion {
|
||||
t.Errorf("version: got %q, want %q", version, tt.wantVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginDiscoverSkipsBuiltins(t *testing.T) {
|
||||
origHandler := pluginHandler
|
||||
defer func() { pluginHandler = origHandler }()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
for _, name := range []string{"flux-get", "flux-create", "flux-version"} {
|
||||
os.WriteFile(pluginDir+"/"+name, []byte("#!/bin/sh"), 0o755)
|
||||
}
|
||||
os.WriteFile(pluginDir+"/flux-myplugin", []byte("#!/bin/sh"), 0o755)
|
||||
|
||||
pluginHandler = &plugin.Handler{
|
||||
ReadDir: os.ReadDir,
|
||||
Stat: os.Stat,
|
||||
GetEnv: func(key string) string {
|
||||
if key == "FLUXCD_PLUGINS" {
|
||||
return pluginDir
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||
}
|
||||
|
||||
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||
|
||||
if len(plugins) != 1 {
|
||||
names := make([]string, len(plugins))
|
||||
for i, p := range plugins {
|
||||
names[i] = p.Name
|
||||
}
|
||||
t.Fatalf("expected 1 plugin, got %d: %v", len(plugins), names)
|
||||
}
|
||||
if plugins[0].Name != "myplugin" {
|
||||
t.Errorf("expected 'myplugin', got %q", plugins[0].Name)
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,14 @@ func reconciliationHandled(kubeClient client.Client, namespacedName types.Namesp
|
||||
return false, err
|
||||
}
|
||||
|
||||
return result.Status == kstatus.CurrentStatus, nil
|
||||
switch result.Status {
|
||||
case kstatus.CurrentStatus:
|
||||
return true, nil
|
||||
case kstatus.InProgressStatus:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("%s", result.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,17 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error {
|
||||
|
||||
resume.printMessage(reconcileResps)
|
||||
|
||||
// Return an error if any reconciliation failed
|
||||
var failedCount int
|
||||
for _, r := range reconcileResps {
|
||||
if r.resumable != nil && r.err != nil {
|
||||
failedCount++
|
||||
}
|
||||
}
|
||||
if failedCount > 0 {
|
||||
return fmt.Errorf("reconciliation failed for %d %s(s)", failedCount, resume.kind)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
2
cmd/flux/testdata/check/check_pre.golden
vendored
2
cmd/flux/testdata/check/check_pre.golden
vendored
@@ -1,3 +1,3 @@
|
||||
► checking prerequisites
|
||||
✔ Kubernetes {{ .serverVersion }} >=1.32.0-0
|
||||
✔ Kubernetes {{ .serverVersion }} >=1.33.0-0
|
||||
✔ prerequisites checks passed
|
||||
|
||||
@@ -26,6 +26,8 @@ The following template can be used for the GitHub release page:
|
||||
|
||||
<!-- Text describing the most important changes in this release -->
|
||||
|
||||
ℹ️ Please follow the [Upgrade Procedure for Flux v2.7+](https://github.com/fluxcd/flux2/discussions/5572) for a smooth upgrade from Flux v2.6 to the latest version.
|
||||
|
||||
### Fixes and improvements
|
||||
|
||||
<!-- List of fixes and improvements to the controllers and CLI -->
|
||||
@@ -36,7 +38,7 @@ The following template can be used for the GitHub release page:
|
||||
|
||||
## Components changelog
|
||||
|
||||
- <name>-controller [v<version>](https://github.com/fluxcd/<name>-controller/blob/<version>/CHANGELOG.md
|
||||
- <name>-controller [v<version>](https://github.com/fluxcd/<name>-controller/blob/<version>/CHANGELOG.md)
|
||||
|
||||
## CLI changelog
|
||||
|
||||
|
||||
157
go.mod
157
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/fluxcd/flux2/v2
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
// Fix CVE-2022-28948.
|
||||
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -8,60 +8,61 @@ replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/ProtonMail/go-crypto v1.3.0
|
||||
github.com/briandowns/spinner v1.23.2
|
||||
github.com/cyphar/filepath-securejoin v0.6.1
|
||||
github.com/distribution/distribution/v3 v3.0.0
|
||||
github.com/fluxcd/cli-utils v0.37.1-flux.1
|
||||
github.com/fluxcd/go-git-providers v0.25.0
|
||||
github.com/fluxcd/helm-controller/api v1.4.5
|
||||
github.com/fluxcd/image-automation-controller/api v1.0.4
|
||||
github.com/fluxcd/image-reflector-controller/api v1.0.4
|
||||
github.com/fluxcd/kustomize-controller/api v1.7.3
|
||||
github.com/fluxcd/notification-controller/api v1.7.5
|
||||
github.com/fluxcd/pkg/apis/event v0.22.0
|
||||
github.com/fluxcd/pkg/apis/meta v1.25.0
|
||||
github.com/fluxcd/pkg/auth v0.36.0
|
||||
github.com/fluxcd/pkg/chartutil v1.21.0
|
||||
github.com/fluxcd/cli-utils v0.37.2-flux.1
|
||||
github.com/fluxcd/go-git-providers v0.26.0
|
||||
github.com/fluxcd/helm-controller/api v1.5.3
|
||||
github.com/fluxcd/image-automation-controller/api v1.1.1
|
||||
github.com/fluxcd/image-reflector-controller/api v1.1.1
|
||||
github.com/fluxcd/kustomize-controller/api v1.8.2
|
||||
github.com/fluxcd/notification-controller/api v1.8.2
|
||||
github.com/fluxcd/pkg/apis/event v0.25.0
|
||||
github.com/fluxcd/pkg/apis/meta v1.26.0
|
||||
github.com/fluxcd/pkg/auth v0.40.0
|
||||
github.com/fluxcd/pkg/chartutil v1.23.0
|
||||
github.com/fluxcd/pkg/envsubst v1.5.0
|
||||
github.com/fluxcd/pkg/git v0.41.0
|
||||
github.com/fluxcd/pkg/kustomize v1.25.0
|
||||
github.com/fluxcd/pkg/oci v0.59.0
|
||||
github.com/fluxcd/pkg/runtime v0.96.0
|
||||
github.com/fluxcd/pkg/sourceignore v0.16.0
|
||||
github.com/fluxcd/pkg/ssa v0.64.0
|
||||
github.com/fluxcd/pkg/git v0.46.0
|
||||
github.com/fluxcd/pkg/kustomize v1.28.0
|
||||
github.com/fluxcd/pkg/oci v0.63.0
|
||||
github.com/fluxcd/pkg/runtime v0.103.0
|
||||
github.com/fluxcd/pkg/sourceignore v0.17.0
|
||||
github.com/fluxcd/pkg/ssa v0.70.0
|
||||
github.com/fluxcd/pkg/ssh v0.24.0
|
||||
github.com/fluxcd/pkg/tar v0.17.0
|
||||
github.com/fluxcd/pkg/version v0.12.0
|
||||
github.com/fluxcd/source-controller/api v1.7.4
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3
|
||||
github.com/go-git/go-git/v5 v5.16.4
|
||||
github.com/fluxcd/pkg/version v0.14.0
|
||||
github.com/fluxcd/source-controller/api v1.8.1
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.1.1
|
||||
github.com/go-git/go-git/v5 v5.16.5
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/gonvenience/bunt v1.4.2
|
||||
github.com/gonvenience/ytbx v1.4.7
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.7
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/homeport/dyff v1.10.2
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/notaryproject/notation-go v1.3.2
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/onsi/gomega v1.39.0
|
||||
github.com/onsi/gomega v1.39.1
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/theckman/yacspin v0.13.12
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/term v0.39.0
|
||||
golang.org/x/text v0.33.0
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apiextensions-apiserver v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/cli-runtime v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/kubectl v0.35.0
|
||||
sigs.k8s.io/controller-runtime v0.23.0
|
||||
sigs.k8s.io/kustomize/api v0.21.0
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/text v0.34.0
|
||||
k8s.io/api v0.35.2
|
||||
k8s.io/apiextensions-apiserver v0.35.2
|
||||
k8s.io/apimachinery v0.35.2
|
||||
k8s.io/cli-runtime v0.35.2
|
||||
k8s.io/client-go v0.35.2
|
||||
k8s.io/kubectl v0.35.2
|
||||
sigs.k8s.io/controller-runtime v0.23.3
|
||||
sigs.k8s.io/kustomize/api v0.21.1
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
@@ -69,7 +70,7 @@ require (
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
code.gitea.io/sdk/gitea v0.22.0 // indirect
|
||||
code.gitea.io/sdk/gitea v0.23.2 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
@@ -107,7 +108,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
@@ -115,7 +116,7 @@ require (
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.1.5+incompatible // indirect
|
||||
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
@@ -128,7 +129,7 @@ require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect
|
||||
github.com/fluxcd/pkg/cache v0.13.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
@@ -150,20 +151,18 @@ require (
|
||||
github.com/gonvenience/text v1.0.9 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-github/v75 v75.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-github/v82 v82.0.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
|
||||
@@ -199,9 +198,9 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/otlptranslator v0.0.2 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
|
||||
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
||||
@@ -217,41 +216,41 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.5 // indirect
|
||||
gitlab.com/gitlab-org/api/client-go v1.29.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/api v0.261.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
@@ -259,12 +258,12 @@ require (
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
helm.sh/helm/v4 v4.1.0 // indirect
|
||||
k8s.io/component-base v0.35.0 // indirect
|
||||
helm.sh/helm/v4 v4.1.3 // indirect
|
||||
k8s.io/component-base v0.35.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
|
||||
)
|
||||
|
||||
332
go.sum
332
go.sum
@@ -4,8 +4,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
|
||||
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
@@ -91,6 +91,8 @@ 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=
|
||||
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
@@ -115,8 +117,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
@@ -142,8 +144,8 @@ github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN
|
||||
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
@@ -168,62 +170,62 @@ 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.37.1-flux.1 h1:WnG2mHxCPZMj/soIq/S/1zvbrGCJN3GJGbNfG06X55M=
|
||||
github.com/fluxcd/cli-utils v0.37.1-flux.1/go.mod h1:aND5wX3LuTFtB7eUT7vsWr8mmxRVSPR2Wkvbn0SqPfw=
|
||||
github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0=
|
||||
github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c=
|
||||
github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg=
|
||||
github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo=
|
||||
github.com/fluxcd/go-git-providers v0.25.0 h1:zkVgujjo2VjKXbucrlTyNhHd9x+27oqyghJX9uLwQv4=
|
||||
github.com/fluxcd/go-git-providers v0.25.0/go.mod h1:8Mx5WRYb61FIjOA26DAi4Ls2rZUHSxP8Nl9qkQHDch8=
|
||||
github.com/fluxcd/helm-controller/api v1.4.5 h1:hMEBtgXUbJjp+ah0jPI3OOQNVngoToOQvTgFgVpAjNg=
|
||||
github.com/fluxcd/helm-controller/api v1.4.5/go.mod h1:rCgx3qhjjtoIH+1EbzFC2vN71/pp0PgMDrZnGCZX5XY=
|
||||
github.com/fluxcd/image-automation-controller/api v1.0.4 h1:Fgdy97hXkyh/JFjxLIyq4ZDHsKsa49aumtrvIyjVd08=
|
||||
github.com/fluxcd/image-automation-controller/api v1.0.4/go.mod h1:LLBf4XQJAgnpIMlZUwfpVIkCdUtBOi31B6fDbPwBCq4=
|
||||
github.com/fluxcd/image-reflector-controller/api v1.0.4 h1:/JGpTZf4eMcKG2FpWfP5H7SneSrD5P8EvwGnHiH/WLY=
|
||||
github.com/fluxcd/image-reflector-controller/api v1.0.4/go.mod h1:5GS4ojHaz+W6hK80WakGIOYk8sn93AyV5X+YOne1XMw=
|
||||
github.com/fluxcd/kustomize-controller/api v1.7.3 h1:g+C9Il+H33DQi/ZiQ8KpTvL9KXebXnS4oM/0uJ/C8Gw=
|
||||
github.com/fluxcd/kustomize-controller/api v1.7.3/go.mod h1:Yj80JyfQpBUgLhsUZ/c86qcvPGO2+P1VCKsb8fL+L/k=
|
||||
github.com/fluxcd/notification-controller/api v1.7.5 h1:6CO5bKyjodiK9exQFOdBcz0XLeo17rrrWQBTJL9NNa8=
|
||||
github.com/fluxcd/notification-controller/api v1.7.5/go.mod h1:IciwSg8Q0pVtdbsyDyEXx/MxBKWeagxAazpm64C8oCE=
|
||||
github.com/fluxcd/go-git-providers v0.26.0 h1:0DUsXc1nS9Fe4n8tXSEUCGemWzHShd66gmotayDPekw=
|
||||
github.com/fluxcd/go-git-providers v0.26.0/go.mod h1:VJDKUOhZwNAIqDF5iPtIpTr/annsDbKMkPpWiDMBdpo=
|
||||
github.com/fluxcd/helm-controller/api v1.5.3 h1:ruLzuyTHjjE9A5B/U+Id2q7yHXXqSFTswdZ14xCS5So=
|
||||
github.com/fluxcd/helm-controller/api v1.5.3/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI=
|
||||
github.com/fluxcd/image-automation-controller/api v1.1.1 h1:uiu7kjdVoW8/461HOemX6I7RcPornEzQliWgTg6LnWI=
|
||||
github.com/fluxcd/image-automation-controller/api v1.1.1/go.mod h1:lkD/drkD6Wc+2SDjVj5KqfozEucTLFexWgby/5ft660=
|
||||
github.com/fluxcd/image-reflector-controller/api v1.1.1 h1:4Bj1abzVnjj8+b/293kNeFMRJc+y2wO8Z12ReZ/gA0w=
|
||||
github.com/fluxcd/image-reflector-controller/api v1.1.1/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0=
|
||||
github.com/fluxcd/kustomize-controller/api v1.8.2 h1:LcFUjJccwNrhCo7pQBBneLAlHfZZcb58bWB2LnyFwag=
|
||||
github.com/fluxcd/kustomize-controller/api v1.8.2/go.mod h1:c/mUPIffDDLg1EicXCJtX4N/rc+z5Zh0e/CXjhd7Dyc=
|
||||
github.com/fluxcd/notification-controller/api v1.8.2 h1:TDrXohUC5Gh3BF+v2ux9/zEG1Ax8u49WDW+3Y6GiIEc=
|
||||
github.com/fluxcd/notification-controller/api v1.8.2/go.mod h1:ozgJGQPy0dG5eOsLZlwAr6n0q/y6+TWd1fGOtavlXJA=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
||||
github.com/fluxcd/pkg/apis/event v0.22.0 h1:nCW0TnneMnscSnj9NlaSKcvyC+436MbY1GyKn/4YnII=
|
||||
github.com/fluxcd/pkg/apis/event v0.22.0/go.mod h1:Hoi4DejaNKVahGkRXqGBjT9h1aKmhc7RCYcsgoTieqc=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0 h1:p8wPIxdmn0vy0a664rsE9JKCfnliZz4HUsDcTy4ZOxA=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0/go.mod h1:XWdsx8P15OiMaQIvmUjYWdmD3zAwhl5q9osl5iCqcOk=
|
||||
github.com/fluxcd/pkg/apis/meta v1.25.0 h1:fmZgMoe7yITGfhFqdOs7w2GOu3Y/2Vvz4+4p/eay3eA=
|
||||
github.com/fluxcd/pkg/apis/meta v1.25.0/go.mod h1:1D92RqAet0/n/cH5S0khBXweirHWkw9rCO0V4NCY6xc=
|
||||
github.com/fluxcd/pkg/auth v0.36.0 h1:4T61EOyRAElhJedwglfa68OxsD6GiNPGGTMZIeYE3sM=
|
||||
github.com/fluxcd/pkg/auth v0.36.0/go.mod h1:pRet9dmeOW3iHEh9BwCvhvjEQ5HjQLi4lblaIfR/yJg=
|
||||
github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE=
|
||||
github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI=
|
||||
github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE=
|
||||
github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0=
|
||||
github.com/fluxcd/pkg/auth v0.40.0 h1:p6Kw6KH+z8oRqngKhmTt8ILKD/rC+8tP87a//kLZhi8=
|
||||
github.com/fluxcd/pkg/auth v0.40.0/go.mod h1:Oq/hIEKUMTbL2bv5blf+EhC/jXXJLsOjIMtJj/AtG3Y=
|
||||
github.com/fluxcd/pkg/cache v0.13.0 h1:MqtlgOwIVcGKKgV422e39O+KFSVMWuExKeRaMDBjJlk=
|
||||
github.com/fluxcd/pkg/cache v0.13.0/go.mod h1:0xRZ1hitrIFQ6pl68ke2wZLbIqA2VLzY78HpDo9DVxs=
|
||||
github.com/fluxcd/pkg/chartutil v1.21.0 h1:NJYhlekwBwuqMpRgsOlcsJrw2Xq0cBJW0Nmvz2oMluA=
|
||||
github.com/fluxcd/pkg/chartutil v1.21.0/go.mod h1:Gv50bF3SS4OvvKCyyIMRkGeNzZk6Fsh4+lAdrjx97T4=
|
||||
github.com/fluxcd/pkg/chartutil v1.23.0 h1:ohstQEVnrBIbN85FGu83hnmAohLl0PdOoPlsM6+cjyI=
|
||||
github.com/fluxcd/pkg/chartutil v1.23.0/go.mod h1:kFhmD6DwBgRsvC1ilINsomargMi2WbqvSndWQLikkLc=
|
||||
github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi1WDY=
|
||||
github.com/fluxcd/pkg/envsubst v1.5.0/go.mod h1:c3a8DYI855sZUubHFYQbjfjop6Wu4/zg1cLyf7SnCes=
|
||||
github.com/fluxcd/pkg/git v0.41.0 h1:WvvIUFssFDKpRrptJjDf0B4mrUCwhesv1Txu3DzTsl8=
|
||||
github.com/fluxcd/pkg/git v0.41.0/go.mod h1:iqR4eZEhd3gdRSkv+VDP3Qz9WCner3aZ5ClkOUe+3fc=
|
||||
github.com/fluxcd/pkg/gittestserver v0.24.0 h1:ZIksyENX8yPlB95GJGoUIT171o2oKFJvFSXu+4mEmzU=
|
||||
github.com/fluxcd/pkg/gittestserver v0.24.0/go.mod h1:9l+gwEfqqe/WxiRvIrQxircgDcXUF3/tw/1Bie/XwJc=
|
||||
github.com/fluxcd/pkg/kustomize v1.25.0 h1:0jjACHxaMif+RYwrlDDqA09vRtib7WbqU8MmF0k91bM=
|
||||
github.com/fluxcd/pkg/kustomize v1.25.0/go.mod h1:253Y78WyQJ+cD1krdoysluy9bsm5yee6SdmA4xf1hnk=
|
||||
github.com/fluxcd/pkg/oci v0.59.0 h1:0b+iy52QEjGE5vZzmlqjlcTTUYtNZ3F70yG6cyKR+Mg=
|
||||
github.com/fluxcd/pkg/oci v0.59.0/go.mod h1:sh3UhBhhKiHBX2Tjnrpq8qPvk28OxPz3hS0iMW6JdOY=
|
||||
github.com/fluxcd/pkg/runtime v0.96.0 h1:sF4ic8131BwbOE+T2pkiXlkr2gCaxAho500zlZJJLck=
|
||||
github.com/fluxcd/pkg/runtime v0.96.0/go.mod h1:FyjNMFNAERkCsF/muTWJYU9MZOsq/m4Sc4aQk/EgQ9E=
|
||||
github.com/fluxcd/pkg/sourceignore v0.16.0 h1:28+IBmNM1rGNQysiAZXyilFMgS0kno/aJM4zSPgqu2A=
|
||||
github.com/fluxcd/pkg/sourceignore v0.16.0/go.mod h1:Enjrk4gdk8t9VEp0dU3OHvMiS5ZHafZiL4H/FGNluh0=
|
||||
github.com/fluxcd/pkg/ssa v0.64.0 h1:B/8VYMIYMeRmolup2HOoWNqXh4UeXi6w2LvXXvl6MZM=
|
||||
github.com/fluxcd/pkg/ssa v0.64.0/go.mod h1:RjvVjJIoRo1ecsv91yMuiqzO6cpNag80M6MOB/vrJdc=
|
||||
github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw=
|
||||
github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI=
|
||||
github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8=
|
||||
github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc=
|
||||
github.com/fluxcd/pkg/kustomize v1.28.0 h1:0RuFVczJRabbt8frHZ/ql8aqte6BOOKk274O09l6/hE=
|
||||
github.com/fluxcd/pkg/kustomize v1.28.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M=
|
||||
github.com/fluxcd/pkg/oci v0.63.0 h1:ZPKTT2C+gWYjhP63xC76iTPdYE9w3ABcsDq77uhAgwo=
|
||||
github.com/fluxcd/pkg/oci v0.63.0/go.mod h1:qMPz4njvm6hJzdyGSb8ydSqrapXxTQwJonxHIsdeXSQ=
|
||||
github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk=
|
||||
github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw=
|
||||
github.com/fluxcd/pkg/sourceignore v0.17.0 h1:Z72nruRMhC15zIEpWoDrAcJcJ1El6QDnP/aRDfE4WOA=
|
||||
github.com/fluxcd/pkg/sourceignore v0.17.0/go.mod h1:3e/VmYLId0pI/H5sK7W9Ibif+j0Ahns9RxNjDMtTTfY=
|
||||
github.com/fluxcd/pkg/ssa v0.70.0 h1:IBylYPiTK1IEdCC2DvjKXIhwQcbd5VufXA9WS3zO+tE=
|
||||
github.com/fluxcd/pkg/ssa v0.70.0/go.mod h1:6igtlt7/zF+nNFQpa5ZAkkvtpL6o36NRU39/PqqC+Bg=
|
||||
github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4=
|
||||
github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg=
|
||||
github.com/fluxcd/pkg/tar v0.17.0 h1:uNxbFXy8ly8C7fJ8D7w3rjTNJFrb4Hp1aY/30XkfvxY=
|
||||
github.com/fluxcd/pkg/tar v0.17.0/go.mod h1:b1xyIRYDD0ket4SV5u0UXYv+ZdN/O/HmIO5jZQdHQls=
|
||||
github.com/fluxcd/pkg/version v0.12.0 h1:MGbdbNf2D5wazMqAkNPn+Lh5j+oY0gxQJFTGyet5Hfc=
|
||||
github.com/fluxcd/pkg/version v0.12.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8=
|
||||
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
|
||||
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
|
||||
github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw=
|
||||
github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8=
|
||||
github.com/fluxcd/source-controller/api v1.8.1 h1:49HiJF5mNEdZTwueQMRahTVts35B+xhN5CsuOAL9gQ0=
|
||||
github.com/fluxcd/source-controller/api v1.8.1/go.mod h1:HgZ6NSH1cyOE2jRoNwln1xEwr9ETvrLeiy1o4O04vQM=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.1.1/go.mod h1:6M1BzBGQRoIuSenSQlfJHwMVVobFPiNPxXqfN0IILc4=
|
||||
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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
@@ -242,8 +244,8 @@ github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9n
|
||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -297,21 +299,20 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/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.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
|
||||
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
|
||||
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
|
||||
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk=
|
||||
github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
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-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -326,12 +327,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
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/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
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=
|
||||
@@ -448,10 +447,10 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
|
||||
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
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.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -485,15 +484,15 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ=
|
||||
github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
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.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
|
||||
@@ -538,8 +537,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
|
||||
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
|
||||
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
|
||||
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
|
||||
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
||||
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
||||
@@ -551,56 +548,56 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
|
||||
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.5 h1:zvengEU958Fjwasi1V+9QNRw0viqNKkqUwvFD15XDZI=
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.5/go.mod h1:Ru5IRauphXt9qwmTzJD7ou1dH7Gc6pnsdFWEiMMpmB0=
|
||||
gitlab.com/gitlab-org/api/client-go v1.29.0 h1:3KnF6vENry/9v9eVrnLi2OfBV0m/WSrwh3RcxgH/hkA=
|
||||
gitlab.com/gitlab-org/api/client-go v1.29.0/go.mod h1:6i3EZtC6gKiTTmDwp+f6r/Yi9OY4AaYubl5B3yXEdHE=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
||||
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
||||
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
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=
|
||||
@@ -622,15 +619,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -646,10 +643,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -686,8 +683,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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=
|
||||
@@ -697,8 +694,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -709,8 +706,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -719,10 +716,9 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
@@ -731,10 +727,10 @@ google.golang.org/api v0.261.0 h1:3DoJ2GGibaCxNi1lhdScNMx9fTW87ujKHDgyHMMYdoA=
|
||||
google.golang.org/api v0.261.0/go.mod h1:nVH0ZK5C4tO0RdsMscleeTLY7I8m/Nt9IXxcXD2tfts=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
@@ -760,39 +756,39 @@ 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/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
helm.sh/helm/v4 v4.1.0 h1:ytBbmQ7W2h1BLMyvkexnoG52JEDbYj9LTnnNgKRhiCI=
|
||||
helm.sh/helm/v4 v4.1.0/go.mod h1:yH4qpYvTNBTHnkRSenhi1m7oEFKoN6iK3/rYyFJ00IQ=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
|
||||
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
||||
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
||||
helm.sh/helm/v4 v4.1.3 h1:Abfmb+oJUtxoaXDyB2Jhw1zRk3hT6aFfHta+AXb8Lno=
|
||||
helm.sh/helm/v4 v4.1.3/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI=
|
||||
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
|
||||
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
|
||||
k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
|
||||
k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
|
||||
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
|
||||
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/cli-runtime v0.35.2 h1:3DNctzpPNXavqyrm/FFiT60TLk4UjUxuUMYbKOE970E=
|
||||
k8s.io/cli-runtime v0.35.2/go.mod h1:G2Ieu0JidLm5m1z9b0OkFhnykvJ1w+vjbz1tR5OFKL0=
|
||||
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
|
||||
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
|
||||
k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc=
|
||||
k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0=
|
||||
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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=
|
||||
k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=
|
||||
k8s.io/kubectl v0.35.2 h1:aSmqhSOfsoG9NR5oR8OD5eMKpLN9x8oncxfqLHbJJII=
|
||||
k8s.io/kubectl v0.35.2/go.mod h1:+OJC779UsDJGxNPbHxCwvb4e4w9Eh62v/DNYU2TlsyM=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.23.0 h1:Ubi7klJWiwEWqDY+odSVZiFA0aDSevOCXpa38yCSYu8=
|
||||
sigs.k8s.io/controller-runtime v0.23.0/go.mod h1:DBOIr9NsprUqCZ1ZhsuJ0wAnQSIxY/C6VjZbmLgw0j0=
|
||||
sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
|
||||
sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/kustomize/api v0.21.0 h1:I7nry5p8iDJbuRdYS7ez8MUvw7XVNPcIP5GkzzuXIIQ=
|
||||
sigs.k8s.io/kustomize/api v0.21.0/go.mod h1:XGVQuR5n2pXKWbzXHweZU683pALGw/AMVO4zU4iS8SE=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
|
||||
sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
|
||||
sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
|
||||
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/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/theckman/yacspin"
|
||||
"github.com/briandowns/spinner"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -81,7 +81,7 @@ type Builder struct {
|
||||
action kustomize.Action
|
||||
kustomization *kustomizev1.Kustomization
|
||||
timeout time.Duration
|
||||
spinner *yacspin.Spinner
|
||||
spinner *spinner.Spinner
|
||||
dryRun bool
|
||||
strictSubst bool
|
||||
recursive bool
|
||||
@@ -111,22 +111,9 @@ func WithTimeout(timeout time.Duration) BuilderOptionFunc {
|
||||
|
||||
func WithProgressBar() BuilderOptionFunc {
|
||||
return func(b *Builder) error {
|
||||
// Add a spinner
|
||||
cfg := yacspin.Config{
|
||||
Frequency: 100 * time.Millisecond,
|
||||
CharSet: yacspin.CharSets[59],
|
||||
Suffix: "Kustomization diffing...",
|
||||
SuffixAutoColon: true,
|
||||
Message: spinnerDryRunMessage,
|
||||
StopCharacter: "✓",
|
||||
StopColors: []string{"fgGreen"},
|
||||
}
|
||||
spinner, err := yacspin.New(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create spinner: %w", err)
|
||||
}
|
||||
b.spinner = spinner
|
||||
|
||||
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||
s.Suffix = " Kustomization diffing... " + spinnerDryRunMessage
|
||||
b.spinner = s
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -215,7 +202,7 @@ func withClientConfigFrom(in *Builder) BuilderOptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// withClientConfigFrom copies spinner field
|
||||
// withSpinnerFrom copies the spinner field from another Builder.
|
||||
func withSpinnerFrom(in *Builder) BuilderOptionFunc {
|
||||
return func(b *Builder) error {
|
||||
b.spinner = in.spinner
|
||||
@@ -746,12 +733,7 @@ func (b *Builder) StartSpinner() error {
|
||||
if b.spinner == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := b.spinner.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start spinner: %w", err)
|
||||
}
|
||||
|
||||
b.spinner.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -759,14 +741,6 @@ func (b *Builder) StopSpinner() error {
|
||||
if b.spinner == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
status := b.spinner.Status()
|
||||
if status == yacspin.SpinnerRunning || status == yacspin.SpinnerPaused {
|
||||
err := b.spinner.Stop()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop spinner: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.spinner.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,14 +173,14 @@ func (b *Builder) diff() (string, bool, error) {
|
||||
|
||||
// finished with Kustomization diff
|
||||
if b.spinner != nil {
|
||||
b.spinner.Message(spinnerDryRunMessage)
|
||||
b.spinner.Suffix = " " + spinnerDryRunMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.spinner != nil {
|
||||
b.spinner.Message("processing inventory")
|
||||
b.spinner.Suffix = " processing inventory"
|
||||
}
|
||||
|
||||
if b.kustomization.Spec.Prune && len(diffErrs) == 0 {
|
||||
@@ -204,7 +204,7 @@ func (b *Builder) diff() (string, bool, error) {
|
||||
|
||||
func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (string, bool, error) {
|
||||
if b.spinner != nil {
|
||||
b.spinner.Message(fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name))
|
||||
b.spinner.Suffix = " " + fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name)
|
||||
}
|
||||
|
||||
sourceRef := kustomization.Spec.SourceRef.DeepCopy()
|
||||
|
||||
213
internal/plugin/catalog.go
Normal file
213
internal/plugin/catalog.go
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCatalogBase = "https://raw.githubusercontent.com/fluxcd/plugins/main/"
|
||||
envCatalogBase = "FLUXCD_PLUGIN_CATALOG"
|
||||
|
||||
pluginAPIVersion = "cli.fluxcd.io/v1beta1"
|
||||
pluginKind = "Plugin"
|
||||
catalogKind = "PluginCatalog"
|
||||
)
|
||||
|
||||
// PluginManifest represents a single plugin's manifest from the catalog.
|
||||
type PluginManifest struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Bin string `json:"bin"`
|
||||
Versions []PluginVersion `json:"versions"`
|
||||
}
|
||||
|
||||
// PluginVersion represents a version entry in a plugin manifest.
|
||||
type PluginVersion struct {
|
||||
Version string `json:"version"`
|
||||
Platforms []PluginPlatform `json:"platforms"`
|
||||
}
|
||||
|
||||
// PluginPlatform represents a platform-specific binary entry.
|
||||
type PluginPlatform struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
URL string `json:"url"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
// PluginCatalog represents the generated catalog.yaml file.
|
||||
type PluginCatalog struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Plugins []CatalogEntry `json:"plugins"`
|
||||
}
|
||||
|
||||
// CatalogEntry is a single entry in the plugin catalog.
|
||||
type CatalogEntry struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
}
|
||||
|
||||
// Receipt records what was installed for a plugin.
|
||||
type Receipt struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
InstalledAt string `json:"installedAt"`
|
||||
Platform PluginPlatform `json:"platform"`
|
||||
}
|
||||
|
||||
// CatalogClient fetches plugin manifests and catalogs from a remote URL.
|
||||
type CatalogClient struct {
|
||||
BaseURL string
|
||||
HTTPClient *http.Client
|
||||
GetEnv func(key string) string
|
||||
}
|
||||
|
||||
// NewCatalogClient returns a CatalogClient with production defaults.
|
||||
func NewCatalogClient() *CatalogClient {
|
||||
return &CatalogClient{
|
||||
BaseURL: defaultCatalogBase,
|
||||
HTTPClient: newHTTPClient(30 * time.Second),
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
}
|
||||
|
||||
// baseURL returns the effective catalog base URL.
|
||||
func (c *CatalogClient) baseURL() string {
|
||||
if env := c.GetEnv(envCatalogBase); env != "" {
|
||||
return env
|
||||
}
|
||||
return c.BaseURL
|
||||
}
|
||||
|
||||
// FetchManifest fetches a single plugin manifest from the catalog.
|
||||
func (c *CatalogClient) FetchManifest(name string) (*PluginManifest, error) {
|
||||
url := c.baseURL() + "plugins/" + name + ".yaml"
|
||||
body, err := c.fetch(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin %q not found in catalog", name)
|
||||
}
|
||||
|
||||
var manifest PluginManifest
|
||||
if err := yaml.Unmarshal(body, &manifest); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse plugin manifest for %q: %w", name, err)
|
||||
}
|
||||
|
||||
if manifest.APIVersion != pluginAPIVersion {
|
||||
return nil, fmt.Errorf("plugin %q has unsupported apiVersion %q (expected %q)", name, manifest.APIVersion, pluginAPIVersion)
|
||||
}
|
||||
if manifest.Kind != pluginKind {
|
||||
return nil, fmt.Errorf("plugin %q has unexpected kind %q (expected %q)", name, manifest.Kind, pluginKind)
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// FetchCatalog fetches the generated catalog.yaml.
|
||||
func (c *CatalogClient) FetchCatalog() (*PluginCatalog, error) {
|
||||
url := c.baseURL() + "catalog.yaml"
|
||||
body, err := c.fetch(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch plugin catalog: %w", err)
|
||||
}
|
||||
|
||||
var catalog PluginCatalog
|
||||
if err := yaml.Unmarshal(body, &catalog); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse plugin catalog: %w", err)
|
||||
}
|
||||
|
||||
if catalog.APIVersion != pluginAPIVersion {
|
||||
return nil, fmt.Errorf("plugin catalog has unsupported apiVersion %q (expected %q)", catalog.APIVersion, pluginAPIVersion)
|
||||
}
|
||||
if catalog.Kind != catalogKind {
|
||||
return nil, fmt.Errorf("plugin catalog has unexpected kind %q (expected %q)", catalog.Kind, catalogKind)
|
||||
}
|
||||
|
||||
return &catalog, nil
|
||||
}
|
||||
|
||||
const maxResponseBytes = 10 << 20 // 10 MiB
|
||||
|
||||
func (c *CatalogClient) fetch(url string) ([]byte, error) {
|
||||
resp, err := c.HTTPClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
|
||||
}
|
||||
|
||||
// newHTTPClient returns a retrying HTTP client with the given timeout.
|
||||
func newHTTPClient(timeout time.Duration) *http.Client {
|
||||
rc := retryablehttp.NewClient()
|
||||
rc.RetryMax = 3
|
||||
rc.Logger = nil
|
||||
c := rc.StandardClient()
|
||||
c.Timeout = timeout
|
||||
return c
|
||||
}
|
||||
|
||||
// ResolveVersion finds the requested version in the manifest.
|
||||
// If version is empty, returns the first (latest) version.
|
||||
func ResolveVersion(manifest *PluginManifest, version string) (*PluginVersion, error) {
|
||||
if len(manifest.Versions) == 0 {
|
||||
return nil, fmt.Errorf("plugin %q has no versions", manifest.Name)
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
return &manifest.Versions[0], nil
|
||||
}
|
||||
|
||||
for i := range manifest.Versions {
|
||||
if manifest.Versions[i].Version == version {
|
||||
return &manifest.Versions[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("version %q not found for plugin %q", version, manifest.Name)
|
||||
}
|
||||
|
||||
// ResolvePlatform finds the platform entry matching the given OS and arch.
|
||||
func ResolvePlatform(pv *PluginVersion, goos, goarch string) (*PluginPlatform, error) {
|
||||
for i := range pv.Platforms {
|
||||
if pv.Platforms[i].OS == goos && pv.Platforms[i].Arch == goarch {
|
||||
return &pv.Platforms[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no binary for %s/%s", goos, goarch)
|
||||
}
|
||||
239
internal/plugin/catalog_test.go
Normal file
239
internal/plugin/catalog_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFetchManifest(t *testing.T) {
|
||||
manifest := `
|
||||
apiVersion: cli.fluxcd.io/v1beta1
|
||||
kind: Plugin
|
||||
name: operator
|
||||
description: Flux Operator CLI
|
||||
bin: flux-operator
|
||||
versions:
|
||||
- version: 0.45.0
|
||||
platforms:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
url: https://example.com/flux-operator_0.45.0_linux_amd64.tar.gz
|
||||
checksum: sha256:abc123
|
||||
`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/plugins/operator.yaml" {
|
||||
w.Write([]byte(manifest))
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &CatalogClient{
|
||||
BaseURL: server.URL + "/",
|
||||
HTTPClient: server.Client(),
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
|
||||
m, err := client.FetchManifest("operator")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if m.Name != "operator" {
|
||||
t.Errorf("expected name 'operator', got %q", m.Name)
|
||||
}
|
||||
if m.Bin != "flux-operator" {
|
||||
t.Errorf("expected bin 'flux-operator', got %q", m.Bin)
|
||||
}
|
||||
if len(m.Versions) != 1 {
|
||||
t.Fatalf("expected 1 version, got %d", len(m.Versions))
|
||||
}
|
||||
if m.Versions[0].Version != "0.45.0" {
|
||||
t.Errorf("expected version '0.45.0', got %q", m.Versions[0].Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchManifestNotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &CatalogClient{
|
||||
BaseURL: server.URL + "/",
|
||||
HTTPClient: server.Client(),
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
|
||||
_, err := client.FetchManifest("nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchCatalog(t *testing.T) {
|
||||
catalog := `
|
||||
apiVersion: cli.fluxcd.io/v1beta1
|
||||
kind: PluginCatalog
|
||||
plugins:
|
||||
- name: operator
|
||||
description: Flux Operator CLI
|
||||
homepage: https://fluxoperator.dev/
|
||||
source: https://github.com/controlplaneio-fluxcd/flux-operator
|
||||
license: AGPL-3.0
|
||||
- name: schema
|
||||
description: CRD schemas
|
||||
homepage: https://example.com/
|
||||
source: https://github.com/example/flux-schema
|
||||
license: Apache-2.0
|
||||
`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/catalog.yaml" {
|
||||
w.Write([]byte(catalog))
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &CatalogClient{
|
||||
BaseURL: server.URL + "/",
|
||||
HTTPClient: server.Client(),
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
|
||||
c, err := client.FetchCatalog()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(c.Plugins) != 2 {
|
||||
t.Fatalf("expected 2 plugins, got %d", len(c.Plugins))
|
||||
}
|
||||
if c.Plugins[0].Name != "operator" {
|
||||
t.Errorf("expected name 'operator', got %q", c.Plugins[0].Name)
|
||||
}
|
||||
if c.Plugins[1].Name != "schema" {
|
||||
t.Errorf("expected name 'schema', got %q", c.Plugins[1].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogEnvOverride(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/custom/catalog.yaml" {
|
||||
w.Write([]byte(`apiVersion: cli.fluxcd.io/v1beta1
|
||||
kind: PluginCatalog
|
||||
plugins: []
|
||||
`))
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &CatalogClient{
|
||||
BaseURL: "https://should-not-be-used/",
|
||||
HTTPClient: server.Client(),
|
||||
GetEnv: func(key string) string {
|
||||
if key == envCatalogBase {
|
||||
return server.URL + "/custom/"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
c, err := client.FetchCatalog()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(c.Plugins) != 0 {
|
||||
t.Fatalf("expected 0 plugins, got %d", len(c.Plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveVersion(t *testing.T) {
|
||||
manifest := &PluginManifest{
|
||||
Name: "operator",
|
||||
Versions: []PluginVersion{
|
||||
{Version: "0.45.0"},
|
||||
{Version: "0.44.0"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("latest", func(t *testing.T) {
|
||||
v, err := ResolveVersion(manifest, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.Version != "0.45.0" {
|
||||
t.Errorf("expected '0.45.0', got %q", v.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("specific", func(t *testing.T) {
|
||||
v, err := ResolveVersion(manifest, "0.44.0")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if v.Version != "0.44.0" {
|
||||
t.Errorf("expected '0.44.0', got %q", v.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
_, err := ResolveVersion(manifest, "0.99.0")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no versions", func(t *testing.T) {
|
||||
_, err := ResolveVersion(&PluginManifest{Name: "empty"}, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolvePlatform(t *testing.T) {
|
||||
pv := &PluginVersion{
|
||||
Version: "0.45.0",
|
||||
Platforms: []PluginPlatform{
|
||||
{OS: "darwin", Arch: "arm64", URL: "https://example.com/darwin_arm64.tar.gz"},
|
||||
{OS: "linux", Arch: "amd64", URL: "https://example.com/linux_amd64.tar.gz"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("found", func(t *testing.T) {
|
||||
p, err := ResolvePlatform(pv, "darwin", "arm64")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if p.OS != "darwin" || p.Arch != "arm64" {
|
||||
t.Errorf("unexpected platform: %s/%s", p.OS, p.Arch)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
_, err := ResolvePlatform(pv, "windows", "amd64")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
75
internal/plugin/completion.go
Normal file
75
internal/plugin/completion.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// commandFunc is an alias to allow DI in tests.
|
||||
var commandFunc = exec.Command
|
||||
|
||||
// CompleteFunc returns a ValidArgsFunction that delegates completion
|
||||
// to the plugin binary via Cobra's __complete protocol.
|
||||
func CompleteFunc(pluginPath string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
completeArgs := append([]string{"__complete"}, args...)
|
||||
completeArgs = append(completeArgs, toComplete)
|
||||
|
||||
out, err := commandFunc(pluginPath, completeArgs...).Output()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
return parseCompletionOutput(string(out))
|
||||
}
|
||||
}
|
||||
|
||||
// parseCompletionOutput parses Cobra's __complete output format.
|
||||
// Each line is a completion, last line is :<directive_int>.
|
||||
func parseCompletionOutput(out string) ([]string, cobra.ShellCompDirective) {
|
||||
out = strings.TrimRight(out, "\n")
|
||||
if out == "" {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
lines := strings.Split(out, "\n")
|
||||
|
||||
// Last line is the directive in format ":N"
|
||||
lastLine := lines[len(lines)-1]
|
||||
completions := lines[:len(lines)-1]
|
||||
|
||||
directive := cobra.ShellCompDirectiveDefault
|
||||
if strings.HasPrefix(lastLine, ":") {
|
||||
if val, err := strconv.Atoi(lastLine[1:]); err == nil {
|
||||
directive = cobra.ShellCompDirective(val)
|
||||
}
|
||||
}
|
||||
|
||||
var results []string
|
||||
for _, c := range completions {
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
results = append(results, c)
|
||||
}
|
||||
|
||||
return results, directive
|
||||
}
|
||||
80
internal/plugin/completion_test.go
Normal file
80
internal/plugin/completion_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestParseCompletionOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedCompletions []string
|
||||
expectedDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
name: "standard output",
|
||||
input: "instance\nrset\nrsip\nall\n:4\n",
|
||||
expectedCompletions: []string{"instance", "rset", "rsip", "all"},
|
||||
expectedDirective: cobra.ShellCompDirective(4),
|
||||
},
|
||||
{
|
||||
name: "default directive",
|
||||
input: "foo\nbar\n:0\n",
|
||||
expectedCompletions: []string{"foo", "bar"},
|
||||
expectedDirective: cobra.ShellCompDirectiveDefault,
|
||||
},
|
||||
{
|
||||
name: "with descriptions",
|
||||
input: "get\tGet resources\nbuild\tBuild resources\n:4\n",
|
||||
expectedCompletions: []string{"get\tGet resources", "build\tBuild resources"},
|
||||
expectedDirective: cobra.ShellCompDirective(4),
|
||||
},
|
||||
{
|
||||
name: "empty completions",
|
||||
input: ":4\n",
|
||||
expectedCompletions: nil,
|
||||
expectedDirective: cobra.ShellCompDirective(4),
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
expectedCompletions: nil,
|
||||
expectedDirective: cobra.ShellCompDirectiveError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
completions, directive := parseCompletionOutput(tt.input)
|
||||
if directive != tt.expectedDirective {
|
||||
t.Errorf("directive: got %d, want %d", directive, tt.expectedDirective)
|
||||
}
|
||||
if len(completions) != len(tt.expectedCompletions) {
|
||||
t.Fatalf("completions count: got %d, want %d", len(completions), len(tt.expectedCompletions))
|
||||
}
|
||||
for i, c := range completions {
|
||||
if c != tt.expectedCompletions[i] {
|
||||
t.Errorf("completion[%d]: got %q, want %q", i, c, tt.expectedCompletions[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
195
internal/plugin/discovery.go
Normal file
195
internal/plugin/discovery.go
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginPrefix = "flux-"
|
||||
defaultDirName = "plugins"
|
||||
defaultBaseDir = ".fluxcd"
|
||||
envPluginDir = "FLUXCD_PLUGINS"
|
||||
)
|
||||
|
||||
// reservedNames are command names that cannot be used as plugin names.
|
||||
var reservedNames = map[string]bool{
|
||||
"plugin": true,
|
||||
"help": true,
|
||||
}
|
||||
|
||||
// Plugin represents a discovered plugin binary.
|
||||
type Plugin struct {
|
||||
Name string // e.g., "operator" (derived from "flux-operator")
|
||||
Path string // absolute path to binary
|
||||
}
|
||||
|
||||
// Handler discovers and executes plugins. Uses dependency injection
|
||||
// for testability.
|
||||
type Handler struct {
|
||||
ReadDir func(name string) ([]os.DirEntry, error)
|
||||
Stat func(name string) (os.FileInfo, error)
|
||||
GetEnv func(key string) string
|
||||
HomeDir func() (string, error)
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler with production defaults.
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
ReadDir: os.ReadDir,
|
||||
Stat: os.Stat,
|
||||
GetEnv: os.Getenv,
|
||||
HomeDir: os.UserHomeDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Discover scans the plugin directory for executables matching flux-*.
|
||||
// It skips builtins, reserved names, directories, non-executable files,
|
||||
// and broken symlinks.
|
||||
func (h *Handler) Discover(builtinNames []string) []Plugin {
|
||||
dir := h.PluginDir()
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries, err := h.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
builtins := make(map[string]bool, len(builtinNames))
|
||||
for _, name := range builtinNames {
|
||||
builtins[name] = true
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if !strings.HasPrefix(name, pluginPrefix) {
|
||||
continue
|
||||
}
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginName := pluginNameFromBinary(name)
|
||||
if pluginName == "" {
|
||||
continue
|
||||
}
|
||||
if reservedNames[pluginName] || builtins[pluginName] {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(dir, name)
|
||||
|
||||
// Use Stat to follow symlinks and check the target.
|
||||
info, err := h.Stat(fullPath)
|
||||
if err != nil {
|
||||
// Broken symlink, permission denied, etc.
|
||||
continue
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
if !isExecutable(info) {
|
||||
continue
|
||||
}
|
||||
|
||||
plugins = append(plugins, Plugin{
|
||||
Name: pluginName,
|
||||
Path: fullPath,
|
||||
})
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
// PluginDir returns the plugin directory path. If FLUXCD_PLUGINS is set,
|
||||
// returns that path. Otherwise returns ~/.fluxcd/plugins/.
|
||||
// Does not create the directory — callers that write (install, update)
|
||||
// should call EnsurePluginDir first.
|
||||
func (h *Handler) PluginDir() string {
|
||||
if dir := h.GetEnv(envPluginDir); dir != "" {
|
||||
return dir
|
||||
}
|
||||
|
||||
home, err := h.HomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(home, defaultBaseDir, defaultDirName)
|
||||
}
|
||||
|
||||
// EnsurePluginDir creates the plugin directory if it doesn't exist
|
||||
// and returns the path. Best-effort — ignores mkdir errors for
|
||||
// read-only filesystems. User-managed directories (via $FLUXCD_PLUGINS)
|
||||
// are not auto-created.
|
||||
func (h *Handler) EnsurePluginDir() string {
|
||||
if envDir := h.GetEnv(envPluginDir); envDir != "" {
|
||||
return envDir
|
||||
}
|
||||
|
||||
home, err := h.HomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
dir := filepath.Join(home, defaultBaseDir, defaultDirName)
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
return dir
|
||||
}
|
||||
|
||||
// pluginNameFromBinary extracts the plugin name from a binary filename.
|
||||
// "flux-operator" → "operator", "flux-my-tool" → "my-tool".
|
||||
// Returns empty string for invalid names.
|
||||
func pluginNameFromBinary(filename string) string {
|
||||
if !strings.HasPrefix(filename, pluginPrefix) {
|
||||
return ""
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(filename, pluginPrefix)
|
||||
|
||||
// On Windows, strip known extensions.
|
||||
if runtime.GOOS == "windows" {
|
||||
for _, ext := range []string{".exe", ".cmd", ".bat"} {
|
||||
if strings.HasSuffix(strings.ToLower(name), ext) {
|
||||
name = name[:len(name)-len(ext)]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// isExecutable checks if a file has the executable bit set.
|
||||
// On Windows, this always returns true (executability is determined by extension).
|
||||
func isExecutable(info os.FileInfo) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return true
|
||||
}
|
||||
return info.Mode().Perm()&0o111 != 0
|
||||
}
|
||||
302
internal/plugin/discovery_test.go
Normal file
302
internal/plugin/discovery_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mockDirEntry implements os.DirEntry for testing.
|
||||
type mockDirEntry struct {
|
||||
name string
|
||||
isDir bool
|
||||
mode fs.FileMode
|
||||
}
|
||||
|
||||
func (m *mockDirEntry) Name() string { return m.name }
|
||||
func (m *mockDirEntry) IsDir() bool { return m.isDir }
|
||||
func (m *mockDirEntry) Type() fs.FileMode { return m.mode }
|
||||
func (m *mockDirEntry) Info() (fs.FileInfo, error) { return nil, nil }
|
||||
|
||||
// mockFileInfo implements os.FileInfo for testing.
|
||||
type mockFileInfo struct {
|
||||
name string
|
||||
mode fs.FileMode
|
||||
isDir bool
|
||||
regular bool
|
||||
}
|
||||
|
||||
func (m *mockFileInfo) Name() string { return m.name }
|
||||
func (m *mockFileInfo) Size() int64 { return 0 }
|
||||
func (m *mockFileInfo) Mode() fs.FileMode { return m.mode }
|
||||
func (m *mockFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (m *mockFileInfo) IsDir() bool { return m.isDir }
|
||||
func (m *mockFileInfo) Sys() any { return nil }
|
||||
|
||||
func newTestHandler(entries []os.DirEntry, statResults map[string]*mockFileInfo, envVars map[string]string) *Handler {
|
||||
return &Handler{
|
||||
ReadDir: func(name string) ([]os.DirEntry, error) {
|
||||
if entries == nil {
|
||||
return nil, fmt.Errorf("directory not found")
|
||||
}
|
||||
return entries, nil
|
||||
},
|
||||
Stat: func(name string) (os.FileInfo, error) {
|
||||
if info, ok := statResults[name]; ok {
|
||||
return info, nil
|
||||
}
|
||||
return nil, fmt.Errorf("file not found: %s", name)
|
||||
},
|
||||
GetEnv: func(key string) string {
|
||||
return envVars[key]
|
||||
},
|
||||
HomeDir: func() (string, error) {
|
||||
return "/home/testuser", nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscover(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||
&mockDirEntry{name: "flux-local", mode: 0},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||
"/test/plugins/flux-local": {name: "flux-local", mode: 0o755},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 2 {
|
||||
t.Fatalf("expected 2 plugins, got %d", len(plugins))
|
||||
}
|
||||
if plugins[0].Name != "operator" {
|
||||
t.Errorf("expected name 'operator', got %q", plugins[0].Name)
|
||||
}
|
||||
if plugins[1].Name != "local" {
|
||||
t.Errorf("expected name 'local', got %q", plugins[1].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSkipsBuiltins(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-version", mode: 0},
|
||||
&mockDirEntry{name: "flux-get", mode: 0},
|
||||
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/test/plugins/flux-version": {name: "flux-version", mode: 0o755},
|
||||
"/test/plugins/flux-get": {name: "flux-get", mode: 0o755},
|
||||
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover([]string{"version", "get"})
|
||||
if len(plugins) != 1 {
|
||||
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||
}
|
||||
if plugins[0].Name != "operator" {
|
||||
t.Errorf("expected name 'operator', got %q", plugins[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSkipsReserved(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-plugin", mode: 0},
|
||||
&mockDirEntry{name: "flux-help", mode: 0},
|
||||
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/test/plugins/flux-plugin": {name: "flux-plugin", mode: 0o755},
|
||||
"/test/plugins/flux-help": {name: "flux-help", mode: 0o755},
|
||||
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 1 {
|
||||
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||
}
|
||||
if plugins[0].Name != "operator" {
|
||||
t.Errorf("expected name 'operator', got %q", plugins[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSkipsNonExecutable(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-noperm", mode: 0},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/test/plugins/flux-noperm": {name: "flux-noperm", mode: 0o644},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 0 {
|
||||
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSkipsDirectories(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-somedir", isDir: true, mode: fs.ModeDir},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 0 {
|
||||
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverFollowsSymlinks(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
// Symlink entry — Type() returns symlink, but Stat resolves to regular executable.
|
||||
&mockDirEntry{name: "flux-linked", mode: fs.ModeSymlink},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/test/plugins/flux-linked": {name: "flux-linked", mode: 0o755},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 1 {
|
||||
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||
}
|
||||
if plugins[0].Name != "linked" {
|
||||
t.Errorf("expected name 'linked', got %q", plugins[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverDirNotExist(t *testing.T) {
|
||||
h := newTestHandler(nil, nil, map[string]string{envPluginDir: "/nonexistent"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 0 {
|
||||
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverCustomDir(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-custom", mode: 0},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/custom/path/flux-custom": {name: "flux-custom", mode: 0o755},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/custom/path"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 1 {
|
||||
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||
}
|
||||
if plugins[0].Path != "/custom/path/flux-custom" {
|
||||
t.Errorf("expected path '/custom/path/flux-custom', got %q", plugins[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverSkipsNonFluxPrefix(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "kubectl-foo", mode: 0},
|
||||
&mockDirEntry{name: "random-binary", mode: 0},
|
||||
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||
}
|
||||
stats := map[string]*mockFileInfo{
|
||||
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||
}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 1 {
|
||||
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverBrokenSymlink(t *testing.T) {
|
||||
entries := []os.DirEntry{
|
||||
&mockDirEntry{name: "flux-broken", mode: fs.ModeSymlink},
|
||||
}
|
||||
// No stat entry for flux-broken — simulates a broken symlink.
|
||||
stats := map[string]*mockFileInfo{}
|
||||
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||
|
||||
plugins := h.Discover(nil)
|
||||
if len(plugins) != 0 {
|
||||
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginNameFromBinary(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"flux-operator", "operator"},
|
||||
{"flux-my-tool", "my-tool"},
|
||||
{"flux-", ""},
|
||||
{"notflux-thing", ""},
|
||||
{"flux-a", "a"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := pluginNameFromBinary(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("pluginNameFromBinary(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginDir(t *testing.T) {
|
||||
t.Run("uses env var", func(t *testing.T) {
|
||||
h := &Handler{
|
||||
GetEnv: func(key string) string {
|
||||
if key == envPluginDir {
|
||||
return "/custom/plugins"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
HomeDir: func() (string, error) {
|
||||
return "/home/user", nil
|
||||
},
|
||||
}
|
||||
dir := h.PluginDir()
|
||||
if dir != "/custom/plugins" {
|
||||
t.Errorf("expected '/custom/plugins', got %q", dir)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses default", func(t *testing.T) {
|
||||
h := &Handler{
|
||||
GetEnv: func(key string) string { return "" },
|
||||
HomeDir: func() (string, error) {
|
||||
return "/home/user", nil
|
||||
},
|
||||
}
|
||||
dir := h.PluginDir()
|
||||
if dir != "/home/user/.fluxcd/plugins" {
|
||||
t.Errorf("expected '/home/user/.fluxcd/plugins', got %q", dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
30
internal/plugin/exec_unix.go
Normal file
30
internal/plugin/exec_unix.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Exec replaces the current process with the plugin binary.
|
||||
// This is what kubectl does — no signal forwarding or exit code propagation needed.
|
||||
func Exec(path string, args []string) error {
|
||||
return syscall.Exec(path, append([]string{path}, args...), os.Environ())
|
||||
}
|
||||
42
internal/plugin/exec_windows.go
Normal file
42
internal/plugin/exec_windows.go
Normal file
@@ -0,0 +1,42 @@
|
||||
//go:build windows
|
||||
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Exec runs the plugin as a child process with full I/O passthrough.
|
||||
// Matches kubectl's Windows fallback pattern.
|
||||
func Exec(path string, args []string) error {
|
||||
cmd := exec.Command(path, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
os.Exit(exitErr.ExitCode())
|
||||
}
|
||||
return err
|
||||
}
|
||||
235
internal/plugin/install.go
Normal file
235
internal/plugin/install.go
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Installer handles downloading, verifying, and installing plugins.
|
||||
type Installer struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewInstaller returns an Installer with production defaults.
|
||||
func NewInstaller() *Installer {
|
||||
return &Installer{
|
||||
HTTPClient: newHTTPClient(5 * time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
// Install downloads, verifies, extracts, and installs a plugin binary
|
||||
// to the given plugin directory.
|
||||
func (inst *Installer) Install(pluginDir string, manifest *PluginManifest, pv *PluginVersion, plat *PluginPlatform) error {
|
||||
tmpFile, err := os.CreateTemp("", "flux-plugin-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close()
|
||||
|
||||
resp, err := inst.HTTPClient.Get(plat.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download plugin: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download plugin: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
writer := io.MultiWriter(tmpFile, hasher)
|
||||
if _, err := io.Copy(writer, resp.Body); err != nil {
|
||||
return fmt.Errorf("failed to download plugin: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
actualChecksum := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
|
||||
if actualChecksum != plat.Checksum {
|
||||
return fmt.Errorf("checksum verification failed (expected: %s, got: %s)", plat.Checksum, actualChecksum)
|
||||
}
|
||||
|
||||
binName := manifest.Bin
|
||||
if runtime.GOOS == "windows" {
|
||||
binName += ".exe"
|
||||
}
|
||||
|
||||
destName := pluginPrefix + manifest.Name
|
||||
if runtime.GOOS == "windows" {
|
||||
destName += ".exe"
|
||||
}
|
||||
destPath := filepath.Join(pluginDir, destName)
|
||||
|
||||
if strings.HasSuffix(plat.URL, ".zip") {
|
||||
err = extractFromZip(tmpFile.Name(), binName, destPath)
|
||||
} else {
|
||||
err = extractFromTarGz(tmpFile.Name(), binName, destPath)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
receipt := Receipt{
|
||||
Name: manifest.Name,
|
||||
Version: pv.Version,
|
||||
InstalledAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Platform: *plat,
|
||||
}
|
||||
return writeReceipt(pluginDir, manifest.Name, &receipt)
|
||||
}
|
||||
|
||||
// Uninstall removes a plugin binary (or symlink) and its receipt from the
|
||||
// plugin directory. Returns an error if the plugin is not installed.
|
||||
func Uninstall(pluginDir, name string) error {
|
||||
binName := pluginPrefix + name
|
||||
if runtime.GOOS == "windows" {
|
||||
binName += ".exe"
|
||||
}
|
||||
|
||||
binPath := filepath.Join(pluginDir, binName)
|
||||
|
||||
// Use Lstat so we detect symlinks without following them.
|
||||
if _, err := os.Lstat(binPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("plugin %q is not installed", name)
|
||||
}
|
||||
|
||||
if err := os.Remove(binPath); err != nil {
|
||||
return fmt.Errorf("failed to remove plugin binary: %w", err)
|
||||
}
|
||||
|
||||
// Receipt is optional (manually installed plugins don't have one).
|
||||
if err := os.Remove(receiptPath(pluginDir, name)); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove plugin receipt: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadReceipt reads the install receipt for a plugin.
|
||||
// Returns nil if no receipt exists.
|
||||
func ReadReceipt(pluginDir, name string) *Receipt {
|
||||
data, err := os.ReadFile(receiptPath(pluginDir, name))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var receipt Receipt
|
||||
if err := yaml.Unmarshal(data, &receipt); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &receipt
|
||||
}
|
||||
|
||||
func receiptPath(pluginDir, name string) string {
|
||||
return filepath.Join(pluginDir, pluginPrefix+name+".yaml")
|
||||
}
|
||||
|
||||
func writeReceipt(pluginDir, name string, receipt *Receipt) error {
|
||||
data, err := yaml.Marshal(receipt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal receipt: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(receiptPath(pluginDir, name), data, 0o644)
|
||||
}
|
||||
|
||||
// extractFromTarGz extracts a named file from a tar.gz archive
|
||||
// and streams it directly to destPath.
|
||||
func extractFromTarGz(archivePath, targetName, destPath string) error {
|
||||
f, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read gzip: %w", err)
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
tr := tar.NewReader(gr)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read tar: %w", err)
|
||||
}
|
||||
|
||||
if filepath.IsAbs(header.Name) || strings.Contains(header.Name, "..") {
|
||||
continue
|
||||
}
|
||||
if filepath.Base(header.Name) == targetName && header.Typeflag == tar.TypeReg {
|
||||
return writeStreamToFile(tr, destPath)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("binary %q not found in archive", targetName)
|
||||
}
|
||||
|
||||
// extractFromZip extracts a named file from a zip archive
|
||||
// and streams it directly to destPath.
|
||||
func extractFromZip(archivePath, targetName, destPath string) error {
|
||||
r, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open zip: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
if filepath.Base(f.Name) == targetName && !f.FileInfo().IsDir() {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q in zip: %w", targetName, err)
|
||||
}
|
||||
defer rc.Close()
|
||||
return writeStreamToFile(rc, destPath)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("binary %q not found in archive", targetName)
|
||||
}
|
||||
|
||||
func writeStreamToFile(r io.Reader, destPath string) error {
|
||||
out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", destPath, err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
out.Close()
|
||||
return fmt.Errorf("failed to write plugin binary: %w", err)
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
331
internal/plugin/install_test.go
Normal file
331
internal/plugin/install_test.go
Normal file
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// createTestTarGz creates a tar.gz archive containing a single file.
|
||||
func createTestTarGz(name string, content []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
gw := gzip.NewWriter(&buf)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
hdr := &tar.Header{
|
||||
Name: name,
|
||||
Mode: 0o755,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tw.Write(content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tw.Close()
|
||||
gw.Close()
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
binaryContent := []byte("#!/bin/sh\necho hello")
|
||||
archive, err := createTestTarGz("flux-operator", binaryContent)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test archive: %v", err)
|
||||
}
|
||||
|
||||
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(archive))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(archive)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
manifest := &PluginManifest{
|
||||
Name: "operator",
|
||||
Bin: "flux-operator",
|
||||
}
|
||||
pv := &PluginVersion{Version: "0.45.0"}
|
||||
plat := &PluginPlatform{
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
URL: server.URL + "/flux-operator_0.45.0_linux_amd64.tar.gz",
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
installer := &Installer{HTTPClient: server.Client()}
|
||||
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||
t.Fatalf("install failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify binary was written.
|
||||
binPath := filepath.Join(pluginDir, "flux-operator")
|
||||
data, err := os.ReadFile(binPath)
|
||||
if err != nil {
|
||||
t.Fatalf("binary not found: %v", err)
|
||||
}
|
||||
if string(data) != string(binaryContent) {
|
||||
t.Errorf("binary content mismatch")
|
||||
}
|
||||
|
||||
// Verify receipt was written.
|
||||
receipt := ReadReceipt(pluginDir, "operator")
|
||||
if receipt == nil {
|
||||
t.Fatal("receipt not found")
|
||||
}
|
||||
if receipt.Version != "0.45.0" {
|
||||
t.Errorf("expected version '0.45.0', got %q", receipt.Version)
|
||||
}
|
||||
if receipt.Name != "operator" {
|
||||
t.Errorf("expected name 'operator', got %q", receipt.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallChecksumMismatch(t *testing.T) {
|
||||
binaryContent := []byte("#!/bin/sh\necho hello")
|
||||
archive, err := createTestTarGz("flux-operator", binaryContent)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test archive: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(archive)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
manifest := &PluginManifest{Name: "operator", Bin: "flux-operator"}
|
||||
pv := &PluginVersion{Version: "0.45.0"}
|
||||
plat := &PluginPlatform{
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
URL: server.URL + "/archive.tar.gz",
|
||||
Checksum: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
}
|
||||
|
||||
installer := &Installer{HTTPClient: server.Client()}
|
||||
err = installer.Install(pluginDir, manifest, pv, plat)
|
||||
if err == nil {
|
||||
t.Fatal("expected checksum error, got nil")
|
||||
}
|
||||
if !bytes.Contains([]byte(err.Error()), []byte("checksum verification failed")) {
|
||||
t.Errorf("expected checksum error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallBinaryNotInArchive(t *testing.T) {
|
||||
// Archive contains "wrong-name" instead of "flux-operator".
|
||||
archive, err := createTestTarGz("wrong-name", []byte("content"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test archive: %v", err)
|
||||
}
|
||||
|
||||
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(archive))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(archive)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
manifest := &PluginManifest{Name: "operator", Bin: "flux-operator"}
|
||||
pv := &PluginVersion{Version: "0.45.0"}
|
||||
plat := &PluginPlatform{
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
URL: server.URL + "/archive.tar.gz",
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
installer := &Installer{HTTPClient: server.Client()}
|
||||
err = installer.Install(pluginDir, manifest, pv, plat)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing binary, got nil")
|
||||
}
|
||||
if !bytes.Contains([]byte(err.Error()), []byte("not found in archive")) {
|
||||
t.Errorf("expected 'not found in archive' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstall(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
// Create fake binary and receipt.
|
||||
binPath := filepath.Join(pluginDir, "flux-testplugin")
|
||||
os.WriteFile(binPath, []byte("binary"), 0o755)
|
||||
receiptPath := filepath.Join(pluginDir, "flux-testplugin.yaml")
|
||||
os.WriteFile(receiptPath, []byte("name: testplugin"), 0o644)
|
||||
|
||||
if err := Uninstall(pluginDir, "testplugin"); err != nil {
|
||||
t.Fatalf("uninstall failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(binPath); !os.IsNotExist(err) {
|
||||
t.Error("binary was not removed")
|
||||
}
|
||||
if _, err := os.Stat(receiptPath); !os.IsNotExist(err) {
|
||||
t.Error("receipt was not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallNonExistent(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
err := Uninstall(pluginDir, "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent plugin, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not installed") {
|
||||
t.Errorf("expected 'is not installed' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlinks require elevated privileges on Windows")
|
||||
}
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
// Create a real binary and symlink it into the plugin dir.
|
||||
realBin := filepath.Join(t.TempDir(), "flux-operator")
|
||||
os.WriteFile(realBin, []byte("real binary"), 0o755)
|
||||
|
||||
linkPath := filepath.Join(pluginDir, "flux-linked")
|
||||
os.Symlink(realBin, linkPath)
|
||||
|
||||
if err := Uninstall(pluginDir, "linked"); err != nil {
|
||||
t.Fatalf("uninstall symlink failed: %v", err)
|
||||
}
|
||||
|
||||
// Symlink should be removed.
|
||||
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
||||
t.Error("symlink was not removed")
|
||||
}
|
||||
// Original binary should still exist.
|
||||
if _, err := os.Stat(realBin); err != nil {
|
||||
t.Error("original binary was removed — symlink removal should not affect target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallManualBinary(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
// Manually copied binary with no receipt.
|
||||
binPath := filepath.Join(pluginDir, "flux-manual")
|
||||
os.WriteFile(binPath, []byte("binary"), 0o755)
|
||||
|
||||
if err := Uninstall(pluginDir, "manual"); err != nil {
|
||||
t.Fatalf("uninstall manual binary failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(binPath); !os.IsNotExist(err) {
|
||||
t.Error("binary was not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadReceipt(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
t.Run("exists", func(t *testing.T) {
|
||||
receiptData := `name: operator
|
||||
version: "0.45.0"
|
||||
installedAt: "2026-03-28T20:05:00Z"
|
||||
platform:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
url: https://example.com/archive.tar.gz
|
||||
checksum: sha256:abc123
|
||||
`
|
||||
os.WriteFile(filepath.Join(pluginDir, "flux-operator.yaml"), []byte(receiptData), 0o644)
|
||||
|
||||
receipt := ReadReceipt(pluginDir, "operator")
|
||||
if receipt == nil {
|
||||
t.Fatal("expected receipt, got nil")
|
||||
}
|
||||
if receipt.Version != "0.45.0" {
|
||||
t.Errorf("expected version '0.45.0', got %q", receipt.Version)
|
||||
}
|
||||
if receipt.Platform.OS != "darwin" {
|
||||
t.Errorf("expected OS 'darwin', got %q", receipt.Platform.OS)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not exists", func(t *testing.T) {
|
||||
receipt := ReadReceipt(pluginDir, "nonexistent")
|
||||
if receipt != nil {
|
||||
t.Error("expected nil receipt")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractFromTarGz(t *testing.T) {
|
||||
content := []byte("test binary content")
|
||||
archive, err := createTestTarGz("flux-operator", content)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create archive: %v", err)
|
||||
}
|
||||
|
||||
tmpFile := filepath.Join(t.TempDir(), "test.tar.gz")
|
||||
os.WriteFile(tmpFile, archive, 0o644)
|
||||
|
||||
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||
if err := extractFromTarGz(tmpFile, "flux-operator", destPath); err != nil {
|
||||
t.Fatalf("extract failed: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(destPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read extracted file: %v", err)
|
||||
}
|
||||
if string(data) != string(content) {
|
||||
t.Errorf("content mismatch: got %q, want %q", string(data), string(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFromTarGzNotFound(t *testing.T) {
|
||||
archive, err := createTestTarGz("other-binary", []byte("content"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create archive: %v", err)
|
||||
}
|
||||
|
||||
tmpFile := filepath.Join(t.TempDir(), "test.tar.gz")
|
||||
os.WriteFile(tmpFile, archive, 0o644)
|
||||
|
||||
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||
err = extractFromTarGz(tmpFile, "flux-operator", destPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
85
internal/plugin/update.go
Normal file
85
internal/plugin/update.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
const (
|
||||
SkipReasonManual = "manually installed"
|
||||
SkipReasonUpToDate = "already up to date"
|
||||
)
|
||||
|
||||
// UpdateResult represents the outcome of updating a single plugin.
|
||||
// When an update is available, Manifest, Version and Platform are
|
||||
// populated so the caller can install without re-fetching or re-resolving.
|
||||
type UpdateResult struct {
|
||||
Name string
|
||||
FromVersion string
|
||||
ToVersion string
|
||||
Skipped bool
|
||||
SkipReason string
|
||||
Manifest *PluginManifest
|
||||
Version *PluginVersion
|
||||
Platform *PluginPlatform
|
||||
Err error
|
||||
}
|
||||
|
||||
// CheckUpdate compares the installed version against the latest in the catalog.
|
||||
// Returns an UpdateResult describing what should happen. When an update is
|
||||
// available, Manifest is populated so the caller can install without re-fetching.
|
||||
func CheckUpdate(pluginDir string, name string, catalog *CatalogClient, goos, goarch string) UpdateResult {
|
||||
receipt := ReadReceipt(pluginDir, name)
|
||||
if receipt == nil {
|
||||
return UpdateResult{
|
||||
Name: name,
|
||||
Skipped: true,
|
||||
SkipReason: SkipReasonManual,
|
||||
}
|
||||
}
|
||||
|
||||
manifest, err := catalog.FetchManifest(name)
|
||||
if err != nil {
|
||||
return UpdateResult{Name: name, Err: err}
|
||||
}
|
||||
|
||||
latest, err := ResolveVersion(manifest, "")
|
||||
if err != nil {
|
||||
return UpdateResult{Name: name, Err: err}
|
||||
}
|
||||
|
||||
if receipt.Version == latest.Version {
|
||||
return UpdateResult{
|
||||
Name: name,
|
||||
FromVersion: receipt.Version,
|
||||
ToVersion: latest.Version,
|
||||
Skipped: true,
|
||||
SkipReason: SkipReasonUpToDate,
|
||||
}
|
||||
}
|
||||
|
||||
plat, err := ResolvePlatform(latest, goos, goarch)
|
||||
if err != nil {
|
||||
return UpdateResult{Name: name, Err: err}
|
||||
}
|
||||
|
||||
return UpdateResult{
|
||||
Name: name,
|
||||
FromVersion: receipt.Version,
|
||||
ToVersion: latest.Version,
|
||||
Manifest: manifest,
|
||||
Version: latest,
|
||||
Platform: plat,
|
||||
}
|
||||
}
|
||||
153
internal/plugin/update_test.go
Normal file
153
internal/plugin/update_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Copyright 2026 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 plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckUpdateUpToDate(t *testing.T) {
|
||||
manifest := `
|
||||
apiVersion: cli.fluxcd.io/v1beta1
|
||||
kind: Plugin
|
||||
name: operator
|
||||
bin: flux-operator
|
||||
versions:
|
||||
- version: 0.45.0
|
||||
platforms:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
url: https://example.com/archive.tar.gz
|
||||
checksum: sha256:abc123
|
||||
`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(manifest))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
// Write receipt with same version.
|
||||
receiptData := `name: operator
|
||||
version: "0.45.0"
|
||||
installedAt: "2026-03-28T20:05:00Z"
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
`
|
||||
os.WriteFile(filepath.Join(pluginDir, "flux-operator.yaml"), []byte(receiptData), 0o644)
|
||||
|
||||
catalog := &CatalogClient{
|
||||
BaseURL: server.URL + "/",
|
||||
HTTPClient: server.Client(),
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
|
||||
result := CheckUpdate(pluginDir, "operator", catalog, "linux", "amd64")
|
||||
if result.Err != nil {
|
||||
t.Fatalf("unexpected error: %v", result.Err)
|
||||
}
|
||||
if !result.Skipped {
|
||||
t.Error("expected skipped=true")
|
||||
}
|
||||
if result.SkipReason != SkipReasonUpToDate {
|
||||
t.Errorf("expected %q, got %q", SkipReasonUpToDate, result.SkipReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUpdateAvailable(t *testing.T) {
|
||||
manifest := `
|
||||
apiVersion: cli.fluxcd.io/v1beta1
|
||||
kind: Plugin
|
||||
name: operator
|
||||
bin: flux-operator
|
||||
versions:
|
||||
- version: 0.46.0
|
||||
platforms:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
url: https://example.com/archive.tar.gz
|
||||
checksum: sha256:abc123
|
||||
- version: 0.45.0
|
||||
platforms:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
url: https://example.com/archive.tar.gz
|
||||
checksum: sha256:def456
|
||||
`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(manifest))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
receiptData := `name: operator
|
||||
version: "0.45.0"
|
||||
installedAt: "2026-03-28T20:05:00Z"
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
`
|
||||
os.WriteFile(filepath.Join(pluginDir, "flux-operator.yaml"), []byte(receiptData), 0o644)
|
||||
|
||||
catalog := &CatalogClient{
|
||||
BaseURL: server.URL + "/",
|
||||
HTTPClient: server.Client(),
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
|
||||
result := CheckUpdate(pluginDir, "operator", catalog, "linux", "amd64")
|
||||
if result.Err != nil {
|
||||
t.Fatalf("unexpected error: %v", result.Err)
|
||||
}
|
||||
if result.Skipped {
|
||||
t.Error("expected skipped=false")
|
||||
}
|
||||
if result.FromVersion != "0.45.0" {
|
||||
t.Errorf("expected from '0.45.0', got %q", result.FromVersion)
|
||||
}
|
||||
if result.ToVersion != "0.46.0" {
|
||||
t.Errorf("expected to '0.46.0', got %q", result.ToVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUpdateManualInstall(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
|
||||
// No receipt — manually installed.
|
||||
catalog := &CatalogClient{
|
||||
BaseURL: "https://example.com/",
|
||||
HTTPClient: http.DefaultClient,
|
||||
GetEnv: func(key string) string { return "" },
|
||||
}
|
||||
|
||||
result := CheckUpdate(pluginDir, "operator", catalog, "linux", "amd64")
|
||||
if result.Err != nil {
|
||||
t.Fatalf("unexpected error: %v", result.Err)
|
||||
}
|
||||
if !result.Skipped {
|
||||
t.Error("expected skipped=true")
|
||||
}
|
||||
if result.SkipReason != SkipReasonManual {
|
||||
t.Errorf("expected 'manually installed', got %q", result.SkipReason)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.4.5/helm-controller.crds.yaml
|
||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.4.5/helm-controller.deployment.yaml
|
||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.crds.yaml
|
||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.0.4/image-automation-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.0.4/image-automation-controller.deployment.yaml
|
||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.0.4/image-reflector-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.0.4/image-reflector-controller.deployment.yaml
|
||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.7.3/kustomize-controller.crds.yaml
|
||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.7.3/kustomize-controller.deployment.yaml
|
||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.crds.yaml
|
||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.7.5/notification-controller.crds.yaml
|
||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.7.5/notification-controller.deployment.yaml
|
||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.crds.yaml
|
||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/source-controller/releases/download/v1.7.4/source-controller.crds.yaml
|
||||
- https://github.com/fluxcd/source-controller/releases/download/v1.7.4/source-controller.deployment.yaml
|
||||
- https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.crds.yaml
|
||||
- https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.0.3/source-watcher.crds.yaml
|
||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.0.3/source-watcher.deployment.yaml
|
||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml
|
||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.deployment.yaml
|
||||
- account.yaml
|
||||
transformers:
|
||||
- labels.yaml
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://github.com/fluxcd/source-controller/releases/download/v1.7.4/source-controller.crds.yaml
|
||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.7.3/kustomize-controller.crds.yaml
|
||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.4.5/helm-controller.crds.yaml
|
||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.7.5/notification-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.0.4/image-reflector-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.0.4/image-automation-controller.crds.yaml
|
||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.0.3/source-watcher.crds.yaml
|
||||
- https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.crds.yaml
|
||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.crds.yaml
|
||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.crds.yaml
|
||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml
|
||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.crds.yaml
|
||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml
|
||||
|
||||
@@ -169,19 +169,19 @@ func BuildWithRoot(root, base string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("%s not found", konfig.DefaultKustomizationFileName())
|
||||
}
|
||||
|
||||
// TODO(hidde): work around for a bug in kustomize causing it to
|
||||
// not properly handle absolute paths on Windows.
|
||||
// Convert the path to a relative path to the working directory
|
||||
// as a temporary fix:
|
||||
// https://github.com/kubernetes-sigs/kustomize/issues/2789
|
||||
// Convert absolute paths to relative when possible, for kustomize
|
||||
// compatibility. If filepath.Rel fails (e.g. paths on different
|
||||
// Windows drives), keep the absolute path — kustomize handles
|
||||
// absolute paths correctly since go-getter was removed.
|
||||
// See: https://github.com/kubernetes-sigs/kustomize/issues/2789
|
||||
// https://github.com/fluxcd/flux2/issues/1153
|
||||
if filepath.IsAbs(base) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base, err = filepath.Rel(wd, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if relBase, err := filepath.Rel(wd, base); err == nil {
|
||||
base = relBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
# RFC-0010 Multi-Tenant Workload Identity
|
||||
|
||||
**Status:** implementable
|
||||
|
||||
<!--
|
||||
Status represents the current state of the RFC.
|
||||
Must be one of `provisional`, `implementable`, `implemented`, `deferred`, `rejected`, `withdrawn`, or `replaced`.
|
||||
-->
|
||||
**Status:** implemented
|
||||
|
||||
**Creation date:** 2025-02-22
|
||||
|
||||
**Last update:** 2025-04-29
|
||||
**Last update:** 2026-03-13
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1420,10 +1415,11 @@ options to call `gcp.NewTokenSource()` and feed this token source to the
|
||||
`HelmRepository` and `HelmChart`, as well as for SOPS decryption
|
||||
in the `Kustomization` API and Azure Event Hubs in the
|
||||
`Provider` API.
|
||||
|
||||
<!--
|
||||
Major milestones in the lifecycle of the RFC such as:
|
||||
- The first Flux release where an initial version of the RFC was available.
|
||||
- The version of Flux where the RFC graduated to general availability.
|
||||
- The version of Flux where the RFC was retired or superseded.
|
||||
-->
|
||||
* In Flux 2.7 object-level workload identity was introduced for all
|
||||
the remaining APIs that support cloud providers, i.e. `Bucket`,
|
||||
`GitRepository` and `ImageUpdateAutomation`, and also all the
|
||||
remaining types for the `Provider` API, i.e. `azuredevops` and
|
||||
`googlepubsub`. In addition, support for controller and
|
||||
object-level workload identity was introduced for the
|
||||
`Kustomization` and `HelmRelease` APIs for remote cluster
|
||||
access.
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
# RFC-0011: OpenTelemetry Tracing
|
||||
|
||||
**Status:** provisional
|
||||
|
||||
<!--
|
||||
Status represents the current state of the RFC.
|
||||
Must be one of `provisional`, `implementable`, `implemented`, `deferred`, `rejected`, `withdrawn`, or `replaced`.
|
||||
-->
|
||||
**Status:** implemented
|
||||
|
||||
**Creation date:** 2025-04-24
|
||||
|
||||
**Last update:** 2025-08-13
|
||||
**Last update:** 2026-03-13
|
||||
|
||||
## Summary
|
||||
The aim is to be able to collect traces via OpenTelemetry (OTel) across all Flux related objects, such as HelmReleases, Kustomizations and among others. These may be sent towards a tracing provider where may be potentially stored and visualized. Flux does not have any responsibility on storing and visualizing those, it keeps being completely stateless. Thereby, being seamless for the user, the implementation is going to be part of the already existing `Alert` API Type. Therefore, `EventSources` is going to discriminate the events belonging to the specific sources, which are going to be looked up to and send them out towards the `Provider` set. In this way, it could facilitate the observability and monitoring of Flux related objects.
|
||||
@@ -210,9 +205,4 @@ This design ensures trace continuity even in challenging distributed environment
|
||||
|
||||
## Implementation History
|
||||
|
||||
<!--
|
||||
Major milestones in the lifecycle of the RFC such as:
|
||||
- The first Flux release where an initial version of the RFC was available.
|
||||
- The version of Flux where the RFC graduated to general availability.
|
||||
- The version of Flux where the RFC was retired or superseded.
|
||||
-->
|
||||
* RFC implemented and generally available in Flux [v2.7.0](https://github.com/fluxcd/flux2/releases/tag/v2.7.0)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# RFC-0012 External Artifact
|
||||
|
||||
**Status:** provisional
|
||||
**Status:** implemented
|
||||
|
||||
**Creation date:** 2025-04-08
|
||||
|
||||
**Last update:** 2025-09-03
|
||||
**Last update:** 2026-03-13
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -319,9 +319,4 @@ control the adoption of the `ExternalArtifact` feature in their clusters.
|
||||
|
||||
## Implementation History
|
||||
|
||||
<!--
|
||||
Major milestones in the lifecycle of the RFC such as:
|
||||
- The first Flux release where an initial version of the RFC was available.
|
||||
- The version of Flux where the RFC graduated to general availability.
|
||||
- The version of Flux where the RFC was retired or superseded.
|
||||
-->
|
||||
* RFC implemented and generally available in Flux [v2.7.0](https://github.com/fluxcd/flux2/releases/tag/v2.7.0)
|
||||
|
||||
@@ -1,660 +0,0 @@
|
||||
# RFC-XXXX Vendor-Agnostic Short-Lived Credentials
|
||||
|
||||
**Status:** provisional
|
||||
|
||||
<!--
|
||||
Status represents the current state of the RFC.
|
||||
Must be one of `provisional`, `implementable`, `implemented`, `deferred`, `rejected`, `withdrawn`, or `replaced`.
|
||||
-->
|
||||
|
||||
**Creation date:** 2026-02-01
|
||||
|
||||
**Last update:** 2026-02-01
|
||||
|
||||
## Summary
|
||||
|
||||
In [RFC-0010](https://github.com/fluxcd/flux2/tree/main/rfcs/0010-multi-tenant-workload-identity)
|
||||
we implemented object-level workload identity for cloud providers leveraging
|
||||
`ServiceAccount` tokens. This RFC proposes extending Flux with vendor-agnostic
|
||||
short-lived credentials based on open standards (OIDC and SPIFFE), enabling
|
||||
workload identity authentication with third-party services that are not
|
||||
cloud-provider-managed, such as self-hosted container registries (e.g.
|
||||
[Zot](https://zotregistry.dev/), [Harbor](https://goharbor.io/)). We propose
|
||||
introducing a new spec field `.spec.credential` to the `OCIRepository` and
|
||||
`ImageRepository` APIs, as a structured object with a `type` sub-field
|
||||
supporting three credential types: `ServiceAccountToken`, `SpiffeJWT` and
|
||||
`SpiffeCertificate`.
|
||||
Once demand for other third-party services supporting these credential
|
||||
standards show up for other Flux APIs, this pattern can be extended further.
|
||||
|
||||
## Motivation
|
||||
|
||||
RFC-0010 introduced multi-tenant workload identity for cloud providers (AWS,
|
||||
Azure, GCP) by associating Flux objects with Kubernetes `ServiceAccounts`.
|
||||
However, the current workload identity support is limited to cloud-provider
|
||||
token exchange through their respective Security Token Services (STS). There
|
||||
is a growing need for Flux to support short-lived credentials for third-party
|
||||
services that implement open standards directly, without depending on any
|
||||
cloud provider.
|
||||
|
||||
Several real-world use cases motivate this work:
|
||||
|
||||
- The `Kustomization` and `HelmRelease` APIs already support a vendor-agnostic
|
||||
form of workload identity through the `generic` provider inside
|
||||
`.spec.kubeConfig.configMapRef` -> `.data.provider`. This generic provider
|
||||
issues a `ServiceAccount` token and uses it directly for authentication with
|
||||
remote Kubernetes clusters configured with external OIDC authentication.
|
||||
However, this pattern has not been extended to other Flux APIs.
|
||||
- Container registries such as [Zot](https://github.com/project-zot/zot/pull/3711)
|
||||
and [Harbor](https://github.com/goharbor/harbor/issues/22027) are implementing
|
||||
OIDC workload identity federation, allowing workloads to authenticate using
|
||||
Kubernetes `ServiceAccount` tokens directly, without cloud provider intermediaries.
|
||||
- The [SPIFFE](https://spiffe.io/) standard provides an alternative identity
|
||||
framework that allows workloads to be identified independently of
|
||||
`ServiceAccounts`, enabling use cases where the identity is the Flux object
|
||||
itself (kind, name, namespace) rather than a Kubernetes `ServiceAccount`.
|
||||
|
||||
### Goals
|
||||
|
||||
- Provide vendor-agnostic short-lived credential support in Flux, starting with
|
||||
the `OCIRepository` and `ImageRepository` APIs.
|
||||
- Support Kubernetes `ServiceAccount` tokens as OIDC credentials for
|
||||
authenticating with third-party services that support OIDC federation.
|
||||
- Support SPIFFE SVIDs (both JWT and x509 certificate) as credentials for
|
||||
authenticating with third-party services that support SPIFFE.
|
||||
- Establish a pattern that can be extended to other Flux APIs in the future.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- It's not a goal to replace or modify the existing cloud-provider workload
|
||||
identity support introduced in RFC-0010. The `.spec.provider` field and its
|
||||
cloud-provider-specific behavior remain unchanged.
|
||||
- It's not a goal to implement a full SPIFFE runtime (SPIRE agent). Instead,
|
||||
Flux controllers will issue short-lived SPIFFE SVIDs directly using a
|
||||
private key provided via a Kubernetes `Secret`.
|
||||
- It's not a goal to support all Flux APIs in the first iteration. The initial
|
||||
implementation targets `OCIRepository` and `ImageRepository`, with other APIs
|
||||
to follow in subsequent releases.
|
||||
|
||||
## Proposal
|
||||
|
||||
We propose introducing a new spec field `.spec.credential` to the `OCIRepository`
|
||||
and `ImageRepository` APIs. This field specifies the type of vendor-agnostic
|
||||
short-lived credential to use for authentication. The field is mutually exclusive
|
||||
with `.spec.provider` (when set to a cloud provider) because "provider" conveys
|
||||
cloud-provider-specific semantics, and because the existing `generic` provider
|
||||
value already has specific behavior in the OCI APIs when used together with
|
||||
`.spec.serviceAccountName` that differs from what is proposed here (see details
|
||||
[here](#why-a-new-field-and-not-specprovider)). The mutual exclusivity is enforced
|
||||
via a CEL validation rule:
|
||||
|
||||
```yaml
|
||||
x-kubernetes-validations:
|
||||
- message: spec.credential can only be used with spec.provider 'generic'
|
||||
rule: '!has(self.credential) || !has(self.provider) || self.provider == ''generic'''
|
||||
```
|
||||
|
||||
### Credential Types
|
||||
|
||||
The `.spec.credential` field is a structured object with the following
|
||||
sub-fields:
|
||||
|
||||
- **`.spec.credential.type`** (string, required): The type of vendor-agnostic
|
||||
short-lived credential to use for authentication.
|
||||
- **`.spec.credential.audiences`** (list of strings, optional): Specifies the
|
||||
audiences (`aud` claim) for the issued JWT when `.spec.credential.type` is
|
||||
`ServiceAccountToken` or `SpiffeJWT`. This allows the third-party service to
|
||||
verify that the token was intended for it. If not specified, defaults to
|
||||
`.spec.url` for `OCIRepository` and `.spec.image` for `ImageRepository`.
|
||||
This is analogous to the `Kustomization` and `HelmRelease` APIs, where
|
||||
the audience defaults to the remote cluster address.
|
||||
|
||||
Grouping credential-related fields under `.spec.credential` keeps the main
|
||||
spec clean and allows extending the credential configuration with additional
|
||||
options in the future without polluting the top-level spec.
|
||||
|
||||
The valid values for `.spec.credential.type` are:
|
||||
|
||||
- **`ServiceAccountToken`**: The controller issues a Kubernetes `ServiceAccount`
|
||||
token and uses it directly as a bearer token for authentication. The
|
||||
`ServiceAccount` is determined by the existing `.spec.serviceAccountName`
|
||||
field, which requires the `ObjectLevelWorkloadIdentity` feature gate
|
||||
(introduced in RFC-0010) to be enabled. If `.spec.serviceAccountName` is
|
||||
not set, the controller's own `ServiceAccount` is used. In multi-tenancy
|
||||
lockdown scenarios, the `--default-service-account` controller flag can be
|
||||
used to force a default `ServiceAccount` when `.spec.serviceAccountName` is
|
||||
not specified, preventing the controller's own `ServiceAccount` from being
|
||||
used. The third-party service must be configured with OIDC federation
|
||||
trusting the Kubernetes `ServiceAccount` token issuer. This is the same
|
||||
mechanism already used by the `generic` provider in the `Kustomization` and
|
||||
`HelmRelease` APIs for remote cluster access.
|
||||
|
||||
- **`SpiffeJWT`**: The controller issues a short-lived SPIFFE SVID JWT where
|
||||
the identity (the `sub` claim) is the Flux object itself, encoded as a
|
||||
[SPIFFE ID](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md)
|
||||
in the format
|
||||
`spiffe://<trust-domain>/<resource>/<namespace>/<name>`, where `<resource>`
|
||||
is the lowercase plural form of the Kubernetes resource type, i.e.
|
||||
`ocirepositories` for `OCIRepository` and `imagerepositories` for
|
||||
`ImageRepository`.
|
||||
The trust domain for the SPIFFE ID is configured via the controller flag
|
||||
`--spiffe-trust-domain`. The `iss` claim is set to the controller flag
|
||||
`--spiffe-issuer`, which third-party services use for OIDC discovery. The
|
||||
JWT is signed using a private key provided via `--spiffe-secret-name`. Unlike
|
||||
`ServiceAccountToken`, this credential type does **not** depend on a
|
||||
Kubernetes `ServiceAccount` for identity. The third-party service must be
|
||||
configured to trust the SPIFFE issuer by having access to the corresponding
|
||||
JWKS document.
|
||||
|
||||
- **`SpiffeCertificate`**: The controller issues a short-lived SPIFFE SVID
|
||||
x509 certificate for client authentication via mTLS. The certificate encodes
|
||||
the same SPIFFE ID as `SpiffeJWT` in the SAN URI field. The certificate is
|
||||
signed using a CA private key and certificate provided via the controller
|
||||
flag `--spiffe-secret-name`. Both `OCIRepository` and `ImageRepository`
|
||||
already support client certificate authentication via mTLS, so this
|
||||
integrates naturally with the existing transport layer. The third-party
|
||||
service must be configured with the CA certificate for trust. Note that
|
||||
`.spec.certSecretRef` may still be optionally used alongside
|
||||
`SpiffeCertificate` for specifying a CA to trust the server's certificate.
|
||||
|
||||
### Controller Flags
|
||||
|
||||
The SPIFFE issuer and cryptographic material are configured as controller-level
|
||||
flags rather than per-object spec fields. This is analogous to the
|
||||
`ServiceAccountToken` credential type, which relies on the cluster-level
|
||||
Kubernetes `ServiceAccount` token issuer PKI — an infrastructure-level concern,
|
||||
not an object-level one. If these inputs were instead provided per-object (e.g.
|
||||
via `.spec.secretRef`), the pattern would degenerate into a secret-based
|
||||
authentication strategy similar to the `github` provider in the Git APIs, which
|
||||
requires a GitHub App private key to be set in `.spec.secretRef`. The goal of
|
||||
this RFC is for Flux objects to have their own identities with short-lived
|
||||
credentials issued from a shared, controller-level PKI, just as Kubernetes
|
||||
`ServiceAccount` tokens are issued from the cluster-level token issuer.
|
||||
|
||||
- **`--spiffe-trust-domain`**: The SPIFFE trust domain used to construct
|
||||
SPIFFE IDs for all SPIFFE credential types. The SPIFFE ID is encoded as
|
||||
`spiffe://<trust-domain>/<resource>/<namespace>/<name>` and is included as the
|
||||
`sub` claim in `SpiffeJWT` tokens and in the SAN URI field of
|
||||
`SpiffeCertificate` x509 certificates. Required when any object uses
|
||||
`SpiffeJWT` or `SpiffeCertificate` as its `.spec.credential`.
|
||||
|
||||
- **`--spiffe-issuer`**: The OIDC issuer URL for `SpiffeJWT` credentials.
|
||||
Used as the `iss` claim in the issued JWTs. Third-party services use this
|
||||
URL for OIDC discovery to fetch the JWKS and verify token signatures.
|
||||
Required when any object uses `SpiffeJWT` as its `.spec.credential`.
|
||||
|
||||
- **`--spiffe-secret-name`**: The name of a Kubernetes TLS `Secret`
|
||||
(`type: kubernetes.io/tls`) in the controller's namespace containing the
|
||||
cryptographic material for issuing SPIFFE SVIDs. This format is compatible
|
||||
with [cert-manager](https://cert-manager.io/), allowing the `Secret` to be
|
||||
automatically provisioned and rotated. The `Secret` must contain:
|
||||
- `tls.key`: The private key. Used for signing JWTs (`SpiffeJWT`) and for
|
||||
signing client certificates (`SpiffeCertificate`).
|
||||
- `tls.crt`: The CA certificate. Used for signing client certificates
|
||||
(`SpiffeCertificate`). Not required for `SpiffeJWT`.
|
||||
|
||||
Required when any object uses `SpiffeJWT` or `SpiffeCertificate` as its
|
||||
`.spec.credential`.
|
||||
|
||||
### User Stories
|
||||
|
||||
#### Story 1
|
||||
|
||||
> As a cluster administrator, I want tenant A to pull OCI artifacts from a
|
||||
> self-hosted Zot registry repository belonging to tenant A using workload
|
||||
> identity, without any cloud provider dependency.
|
||||
|
||||
For example, I would like to have the following configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: tenant-a-repo
|
||||
namespace: tenant-a
|
||||
spec:
|
||||
url: oci://zot.zot.svc.cluster.local:5000/tenant-a
|
||||
credential:
|
||||
type: ServiceAccountToken
|
||||
audiences:
|
||||
- zot.zot.svc.cluster.local
|
||||
serviceAccountName: tenant-a-sa
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: tenant-a-sa
|
||||
namespace: tenant-a
|
||||
```
|
||||
|
||||
The Zot registry is configured with OIDC federation trusting the Kubernetes
|
||||
`ServiceAccount` token issuer, and an authorization policy granting the
|
||||
`ServiceAccount` `tenant-a/tenant-a-sa` access only to the `tenant-a`
|
||||
repository.
|
||||
|
||||
#### Story 2
|
||||
|
||||
> As a cluster administrator, I want to authenticate Flux objects with a
|
||||
> SPIFFE-aware service using the identity of the Flux object itself, not a
|
||||
> `ServiceAccount`.
|
||||
|
||||
For example, I would like to have the following configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: production
|
||||
spec:
|
||||
url: oci://registry.example.com/my-app
|
||||
credential:
|
||||
type: SpiffeJWT
|
||||
audiences:
|
||||
- registry.example.com
|
||||
```
|
||||
|
||||
The controller is started with `--spiffe-trust-domain=example.com`.
|
||||
The SPIFFE ID for this object would be
|
||||
`spiffe://example.com/ocirepositories/production/my-app`,
|
||||
and the registry would authorize access based on this identity.
|
||||
|
||||
#### Story 3
|
||||
|
||||
> As a cluster administrator, I want to use mTLS with a SPIFFE certificate
|
||||
> to authenticate with a container registry that supports SPIFFE-based client
|
||||
> certificate authentication.
|
||||
|
||||
For example, I would like to have the following configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: secure-app
|
||||
namespace: production
|
||||
spec:
|
||||
url: oci://registry.example.com/secure-app
|
||||
credential:
|
||||
type: SpiffeCertificate
|
||||
```
|
||||
|
||||
The controller is started with `--spiffe-trust-domain=example.com`.
|
||||
It issues a short-lived x509 certificate with the SPIFFE ID
|
||||
`spiffe://example.com/ocirepositories/production/secure-app`
|
||||
in the SAN URI field, and uses it for mTLS authentication with the registry.
|
||||
|
||||
#### Story 4
|
||||
|
||||
> As a cluster administrator, I want to use `ServiceAccount` token
|
||||
> authentication to scan container images from a Harbor registry that supports
|
||||
> OIDC workload identity federation.
|
||||
|
||||
```yaml
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: apps
|
||||
spec:
|
||||
image: harbor.example.com/apps/my-app
|
||||
credential:
|
||||
type: ServiceAccountToken
|
||||
audiences:
|
||||
- harbor.example.com
|
||||
serviceAccountName: my-app-sa
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: my-app-sa
|
||||
namespace: apps
|
||||
```
|
||||
|
||||
### Why a New Field and Not `.spec.provider`
|
||||
|
||||
- The word "provider" suggests a cloud provider, which is not what this feature targets.
|
||||
"Credential" better communicates that we are implementing authentication standards, not
|
||||
targeting cloud vendors.
|
||||
- Using only `.spec.provider: generic`, like in the `Kustomization` and `HelmRelease` APIs,
|
||||
is not viable because this value already has specific behavior in the OCI APIs when used
|
||||
together with `.spec.serviceAccountName`, it means: "use `.imagePullSecrets` from the
|
||||
`ServiceAccount` referenced by `.spec.serviceAccountName`".
|
||||
|
||||
The `Kustomization` and `HelmRelease` APIs have a naming inconsistency where
|
||||
`provider: generic` in `.spec.kubeConfig.configMapRef` is used for
|
||||
`ServiceAccountToken`-based authentication with remote clusters. Updating this
|
||||
ConfigMap API is out-of-scope for this RFC; the existing behavior remains
|
||||
unchanged and is considered an exception to the standard proposed here.
|
||||
|
||||
At the end of the day, `generic` is the default provider if none is specified,
|
||||
and it will be the only accepted provider when using `.spec.credential`. This
|
||||
is semantically correct because standard-based credentials are vendor-agnostic
|
||||
by definition, and therefore a "generic" provider conveys the intended meaning.
|
||||
|
||||
### How Trust Should Be Established
|
||||
|
||||
#### For `ServiceAccountToken`
|
||||
|
||||
The Kubernetes `ServiceAccount` token issuer already implements the OIDC
|
||||
Discovery protocol. Third-party services must be configured to trust this
|
||||
issuer by having network access to the OIDC discovery and JWKS endpoints, or
|
||||
by having the JWKS document configured out-of-band. This is the same trust
|
||||
model used by cloud providers for workload identity (see
|
||||
[RFC-0010 Technical Background](https://github.com/fluxcd/flux2/tree/main/rfcs/0010-multi-tenant-workload-identity#technical-background)).
|
||||
For clusters without a built-in public issuer URL, the responsibility of
|
||||
serving the OIDC discovery and JWKS documents can be taken away from the
|
||||
kube-apiserver by using the `--service-account-issuer` and
|
||||
`--service-account-jwks-uri` apiserver flags to point to externally hosted
|
||||
documents. Signing key rotation is also possible through the
|
||||
`--service-account-signing-key-file` and `--service-account-key-file`
|
||||
apiserver flags (see
|
||||
[Flux cross-cloud integration docs](https://fluxcd.io/flux/integrations/cross-cloud/#for-clusters-without-a-built-in-public-issuer-url)).
|
||||
|
||||
#### For `SpiffeJWT`
|
||||
|
||||
`SpiffeJWT` is also OIDC-based — it is an extension of the same OIDC trust
|
||||
model used by `ServiceAccountToken`, but with a separate issuer and key
|
||||
material managed by the cluster administrator rather than by the Kubernetes API
|
||||
server. The trust establishment follows the same OIDC Discovery protocol: users
|
||||
must host the issuer and JWKS documents at the URL specified by
|
||||
`--spiffe-issuer`, reachable from the third-party services they are integrating
|
||||
with. The controller signs the JWTs with the private key from
|
||||
`--spiffe-secret-name`, and the corresponding public key must be available in
|
||||
the JWKS document at the issuer URL.
|
||||
|
||||
#### For `SpiffeCertificate`
|
||||
|
||||
Users must configure the third-party service with the CA certificate provided
|
||||
via `--spiffe-secret-name` so it can verify client certificates issued by the
|
||||
Flux controller.
|
||||
|
||||
### Alternatives
|
||||
|
||||
#### Using `.spec.provider` Instead of `.spec.credential`
|
||||
|
||||
An alternative would be to add the new credential types as values of
|
||||
`.spec.provider` directly (e.g. `provider: serviceaccounttoken`,
|
||||
`provider: spiffejwt`), rather than introducing a separate `.spec.credential`
|
||||
field. However, `.spec.provider` conveys cloud-provider-specific semantics and
|
||||
uses lowercase values like `aws`, `azure`, `gcp` — mixing vendor-agnostic
|
||||
credential types into this field would blur the distinction between targeting a
|
||||
cloud provider and using an open standard. Additionally, the SPIFFE credential
|
||||
types do not require a `ServiceAccount` at all, which is fundamentally
|
||||
different from how `.spec.provider` operates today.
|
||||
|
||||
#### Using an External SPIFFE Runtime (SPIRE)
|
||||
|
||||
Instead of issuing SPIFFE SVIDs directly, we could require users to deploy a
|
||||
full SPIRE agent and have the Flux controllers obtain SVIDs through the SPIFFE
|
||||
Workload API or Delegated Identity API. However, the standard
|
||||
[Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md)
|
||||
only returns the identity assigned to the calling workload — it does not allow
|
||||
the caller to request a specific SPIFFE ID. SPIRE's
|
||||
[Delegated Identity API](https://spiffe.io/docs/latest/deploying/spire_agent/#delegated-identity-api)
|
||||
does allow a trusted delegate (like a Flux controller) to obtain SVIDs on
|
||||
behalf of other workloads, but those identities must still match pre-registered
|
||||
entries on the SPIRE Server. This means a cluster administrator would need to
|
||||
pre-register a SPIRE entry for every Flux object, which would be a significant
|
||||
operational burden. Beyond this, deploying a full SPIRE infrastructure solely
|
||||
for Flux authentication is a heavy dependency that many users would not want.
|
||||
Our approach of issuing SVIDs directly from a provided private key is simpler
|
||||
and self-contained, while still producing standard SPIFFE SVIDs that any
|
||||
SPIFFE-aware service can verify.
|
||||
|
||||
## Design Details
|
||||
|
||||
### API Changes
|
||||
|
||||
For the `OCIRepository` API, we introduce the following new fields:
|
||||
|
||||
```go
|
||||
// Credential specifies the configuration for vendor-agnostic short-lived
|
||||
// credentials.
|
||||
type Credential struct {
|
||||
// Type specifies the type of credential to use for authentication.
|
||||
// +required
|
||||
Type string `json:"type"`
|
||||
|
||||
// Audiences specifies the audiences for the issued JWT when Type is
|
||||
// ServiceAccountToken or SpiffeJWT. Defaults to .spec.url for
|
||||
// OCIRepository and .spec.image for ImageRepository.
|
||||
// +optional
|
||||
Audiences []string `json:"audiences,omitempty"`
|
||||
}
|
||||
|
||||
type OCIRepositorySpec struct {
|
||||
// ... existing fields ...
|
||||
|
||||
// Credential specifies the vendor-agnostic short-lived credential
|
||||
// configuration for authentication. Mutually exclusive with using
|
||||
// .spec.provider for cloud-provider workload identity and with
|
||||
// .spec.secretRef.
|
||||
// +optional
|
||||
Credential *Credential `json:"credential,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
Equivalent fields are introduced for the `ImageRepository` API.
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- `.spec.credential` is mutually exclusive with `.spec.provider` when the
|
||||
provider is set to anything other than `generic`.
|
||||
- `.spec.credential` is mutually exclusive with `.spec.secretRef`.
|
||||
- `.spec.credential.type` is required when `.spec.credential` is specified.
|
||||
- `.spec.credential.audiences` is optional. When not specified and
|
||||
`.spec.credential.type` is `ServiceAccountToken` or `SpiffeJWT`, the
|
||||
audience defaults to `.spec.url` for `OCIRepository` and `.spec.image`
|
||||
for `ImageRepository`.
|
||||
- When `.spec.credential.type` is `SpiffeJWT`, the controller flags
|
||||
`--spiffe-trust-domain`, `--spiffe-issuer` and `--spiffe-secret-name` must
|
||||
be set, otherwise a terminal error is returned.
|
||||
- When `.spec.credential.type` is `SpiffeCertificate`, the controller flags
|
||||
`--spiffe-trust-domain` and `--spiffe-secret-name` must be set, otherwise a
|
||||
terminal error is returned.
|
||||
- When `.spec.credential.type` is `ServiceAccountToken`,
|
||||
`.spec.serviceAccountName` is optional. If not set, the controller's own
|
||||
`ServiceAccount` is used. Using `.spec.serviceAccountName` requires the
|
||||
`ObjectLevelWorkloadIdentity` feature gate (introduced in RFC-0010) to be
|
||||
enabled. In multi-tenancy lockdown scenarios, the
|
||||
`--default-service-account` controller flag can be used to force a default
|
||||
`ServiceAccount` when `.spec.serviceAccountName` is not specified, preventing
|
||||
the controller's own `ServiceAccount` from being used.
|
||||
|
||||
### Credential Issuance
|
||||
|
||||
#### `ServiceAccountToken`
|
||||
|
||||
The controller uses the Kubernetes `TokenRequest` API to issue a short-lived
|
||||
token for the configured `ServiceAccount` (or the controller's own
|
||||
`ServiceAccount` if none is configured). The token is issued with the audiences
|
||||
specified in `.spec.credential.audiences`. The token is then used as a bearer token in the
|
||||
`Authorization` header when authenticating with the third-party service (e.g. a
|
||||
container registry).
|
||||
|
||||
This reuses the same `ServiceAccount` token creation logic already present in
|
||||
the `github.com/fluxcd/pkg/auth` library from RFC-0010, but skips the cloud
|
||||
provider token exchange step.
|
||||
|
||||
#### `SpiffeJWT`
|
||||
|
||||
The controller constructs a JWT with the following claims:
|
||||
|
||||
- `iss`: The value of `--spiffe-issuer`.
|
||||
- `sub`: The SPIFFE ID in the format
|
||||
`spiffe://<trust-domain>/<resource>/<namespace>/<name>`, where the
|
||||
trust domain is the value of `--spiffe-trust-domain`.
|
||||
- `aud`: The values from `.spec.credential.audiences`.
|
||||
- `exp`: One hour from the current time.
|
||||
- `nbf`: The current time.
|
||||
- `iat`: The current time.
|
||||
- `jti`: A unique token identifier.
|
||||
|
||||
The JWT is signed using the private key from the `Secret` referenced by
|
||||
`--spiffe-secret-name`. The signing algorithm is determined by the key type
|
||||
(e.g. RS256 for RSA, ES256 for EC P-256).
|
||||
|
||||
#### `SpiffeCertificate`
|
||||
|
||||
The controller generates a short-lived x509 certificate with:
|
||||
|
||||
- The SPIFFE ID in the SAN URI field.
|
||||
- A validity period of one hour.
|
||||
- Signed by the CA from the `Secret` referenced by `--spiffe-secret-name`.
|
||||
|
||||
The certificate and a freshly generated private key are used for mTLS
|
||||
authentication with the third-party service.
|
||||
|
||||
### Integration with the `auth` Library
|
||||
|
||||
We propose extending the `github.com/fluxcd/pkg/auth` library to support
|
||||
vendor-agnostic credentials alongside the existing cloud-provider workload
|
||||
identity. This involves:
|
||||
|
||||
1. Renaming the existing `auth/generic` package to `auth/serviceaccounttoken`.
|
||||
2. Introducing `auth/spiffejwt` for issuing SPIFFE SVID JWTs, using a JWT
|
||||
library (e.g. `golang.org/x/oauth2/jws` or `github.com/go-jose/go-jose`)
|
||||
and standard Go crypto libraries (`crypto/rsa`, `crypto/ecdsa`) for signing.
|
||||
3. Introducing `auth/spiffecertificate` for issuing SPIFFE SVID x509
|
||||
certificates, using standard Go crypto libraries (`crypto/x509`,
|
||||
`crypto/rsa`, `crypto/ecdsa`, `encoding/pem`) for certificate generation
|
||||
and signing.
|
||||
|
||||
### CLI Support
|
||||
|
||||
The `flux push artifact` command (and related commands: `list`, `pull`, `tag`,
|
||||
`diff`) will support the following credential types via the `--creds` flag:
|
||||
|
||||
#### `ServiceAccountToken`
|
||||
|
||||
Two modes are supported:
|
||||
|
||||
- **Token creation via `TokenRequest` API**: The CLI creates a short-lived
|
||||
`ServiceAccount` token using the Kubernetes API. This mode works both inside
|
||||
a pod (e.g. in a CI/CD pipeline running in-cluster) and locally with a
|
||||
kubeconfig.
|
||||
|
||||
```shell
|
||||
flux push artifact --creds=ServiceAccountToken \
|
||||
--audiences=aud1,aud2 \
|
||||
--sa-name=my-sa
|
||||
```
|
||||
|
||||
If `--sa-name` is not specified and the CLI is running inside a pod, it
|
||||
reads the mounted `ServiceAccount` token file to determine the
|
||||
`ServiceAccount` name. The `auth` library already has logic for finding
|
||||
the current `ServiceAccount` when running inside a pod (used for issuing
|
||||
controller-level tokens), so this can be reused by the CLI. If `--audiences`
|
||||
is not specified, it defaults to the artifact URL. Note that this mode
|
||||
requires the `create` verb on the `serviceaccounts/token` subresource for
|
||||
the target `ServiceAccount`.
|
||||
|
||||
- **Pre-existing token file**: The CLI reads a `ServiceAccount` token from a
|
||||
file. This mode is useful when the token has already been obtained through
|
||||
other means, e.g. a
|
||||
[projected volume](https://kubernetes.io/docs/concepts/storage/projected-volumes/#serviceaccounttoken).
|
||||
|
||||
```shell
|
||||
flux push artifact --creds=ServiceAccountToken \
|
||||
--sa-token=/path/to/token
|
||||
```
|
||||
|
||||
#### `SpiffeJWT` and `SpiffeCertificate`
|
||||
|
||||
In the controller, SPIFFE SVIDs are minted directly by the controller with
|
||||
identities tied to Flux objects. In the CLI, there is no Flux object, and
|
||||
providing the private signing key to the CLI would undermine the security model.
|
||||
Instead, the CLI integrates with SPIRE via the
|
||||
[SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md)
|
||||
using the [`go-spiffe`](https://github.com/spiffe/go-spiffe) library. In this
|
||||
model, SPIRE assigns the identity to the calling workload (e.g. a CI runner
|
||||
pod) based on its registration entries — the caller does not choose the SPIFFE
|
||||
ID. These are complementary identity models: the controller mints SVIDs for
|
||||
Flux objects, while the CLI relies on SPIRE to attest the workload running the
|
||||
CLI.
|
||||
|
||||
For JWT SVIDs, the CLI calls `workloadapi.FetchJWTSVID()` with the specified
|
||||
audiences:
|
||||
|
||||
```shell
|
||||
flux push artifact --creds=SpiffeJWT \
|
||||
--audiences=aud1,aud2
|
||||
```
|
||||
|
||||
If `--audiences` is not specified, it defaults to the artifact URL.
|
||||
|
||||
For x509 SVIDs, the CLI calls `workloadapi.FetchX509SVID()` and uses the
|
||||
returned certificate and private key for mTLS client authentication:
|
||||
|
||||
```shell
|
||||
flux push artifact --creds=SpiffeCertificate
|
||||
```
|
||||
|
||||
Both communicate with the SPIRE Agent via a Unix domain socket configured
|
||||
through the `SPIFFE_ENDPOINT_SOCKET` environment variable.
|
||||
|
||||
Note that adding `github.com/spiffe/go-spiffe/v2` as a dependency to the CLI
|
||||
binary pulls in gRPC (for the Workload API socket communication), which has a
|
||||
non-trivial impact on binary size.
|
||||
|
||||
#### `GitHubOIDCToken`
|
||||
|
||||
As an exception to the "vendor-agnostic" scope of this RFC, the CLI will also
|
||||
support GitHub Actions OIDC tokens. This is motivated by the fact that GitHub
|
||||
Actions is a widely used CI/CD platform and its OIDC token API provides a
|
||||
convenient way to obtain short-lived tokens without managing secrets. The CLI
|
||||
obtains the token from the
|
||||
[GitHub Actions OIDC Token API](https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token).
|
||||
|
||||
```shell
|
||||
flux push artifact --creds=GitHubOIDCToken \
|
||||
--audiences=aud1,aud2
|
||||
```
|
||||
|
||||
If `--audiences` is not specified, it defaults to the artifact URL.
|
||||
|
||||
### Cache Considerations
|
||||
|
||||
The existing token cache from RFC-0010 can be reused. Following the same
|
||||
principle established there, any new fields that interfere with the
|
||||
creation of the credential will be included in the cache key.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- **Private key protection**: The `Secret` referenced by
|
||||
`--spiffe-secret-name` contains sensitive cryptographic material. It must be
|
||||
protected with appropriate RBAC and ideally managed through a secrets
|
||||
management solution. The controller should only have read access to this
|
||||
`Secret`.
|
||||
- **Short-lived credentials**: All credential types produce short-lived tokens
|
||||
or certificates (with a validity of one hour), limiting the blast radius if a
|
||||
credential is leaked.
|
||||
- **SPIFFE ID namespacing**: The SPIFFE ID includes the namespace and object
|
||||
resource/name, providing natural multi-tenant isolation. A tenant's Flux
|
||||
objects will always have SPIFFE IDs scoped to their namespace.
|
||||
- **SPIFFE ID uniqueness**: The SPIFFE ID encodes the resource type, namespace
|
||||
and name of the Flux object, which are unique within a cluster. This
|
||||
guarantees that no two objects can have the same SPIFFE identity, preventing
|
||||
one object from impersonating another (which is otherwise possible if using
|
||||
a `ServiceAccount`).
|
||||
- **Trust boundary**: When using `ServiceAccountToken`, the trust boundary is
|
||||
the same as RFC-0010 (Kubernetes `ServiceAccount` token issuer). When using
|
||||
SPIFFE credentials, the trust boundary extends to whoever has access to the
|
||||
private key referenced by `--spiffe-secret-name`. This key should be
|
||||
treated with the same level of care as the Kubernetes CA key.
|
||||
|
||||
### Proof of Concept
|
||||
|
||||
A proof of concept for the `ServiceAccountToken` credential type was tested
|
||||
end-to-end and validated with two CNCF container registry projects:
|
||||
[Harbor](https://goharbor.io/) and [Zot](https://zotregistry.dev/).
|
||||
|
||||
## Implementation History
|
||||
|
||||
<!--
|
||||
Major milestones in the lifecycle of the RFC such as:
|
||||
- The first Flux release where an initial version of the RFC was available.
|
||||
- The version of Flux where the RFC graduated to general availability.
|
||||
- The version of Flux where the RFC was retired or superseded.
|
||||
-->
|
||||
@@ -88,6 +88,7 @@ metadata:
|
||||
log.Printf("failed to delete resources in '%s' namespace: %s", testID, err)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) })
|
||||
|
||||
g.Eventually(func() bool {
|
||||
err := verifyGitAndKustomization(ctx, testEnv, testID, testID)
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
func TestFluxInstallation(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
ctx := context.TODO()
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, "flux-system") })
|
||||
g.Eventually(func() bool {
|
||||
err := verifyGitAndKustomization(ctx, testEnv.Client, "flux-system", "flux-system")
|
||||
if err != nil {
|
||||
@@ -114,6 +115,7 @@ metadata:
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
ctx := context.TODO()
|
||||
ref := &sourcev1.GitRepositoryRef{
|
||||
Branch: branchName,
|
||||
}
|
||||
@@ -143,6 +145,7 @@ metadata:
|
||||
t.Logf("failed to delete resources in '%s' namespace: %s", tt.name, err)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) })
|
||||
|
||||
g.Eventually(func() bool {
|
||||
err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/fluxcd/flux2/tests/integration
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/pubsub v1.50.1
|
||||
@@ -11,23 +11,23 @@ require (
|
||||
github.com/fluxcd/image-reflector-controller/api v1.0.4
|
||||
github.com/fluxcd/kustomize-controller/api v1.7.3
|
||||
github.com/fluxcd/notification-controller/api v1.7.5
|
||||
github.com/fluxcd/pkg/apis/event v0.22.0
|
||||
github.com/fluxcd/pkg/apis/meta v1.25.0
|
||||
github.com/fluxcd/pkg/git v0.41.0
|
||||
github.com/fluxcd/pkg/runtime v0.96.0
|
||||
github.com/fluxcd/pkg/apis/event v0.25.0
|
||||
github.com/fluxcd/pkg/apis/meta v1.26.0
|
||||
github.com/fluxcd/pkg/git v0.46.0
|
||||
github.com/fluxcd/pkg/runtime v0.103.0
|
||||
github.com/fluxcd/source-controller/api v1.7.4
|
||||
github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b
|
||||
github.com/go-git/go-git/v5 v5.16.4
|
||||
github.com/go-git/go-git/v5 v5.16.5
|
||||
github.com/google/go-containerregistry v0.20.7
|
||||
github.com/hashicorp/terraform-exec v0.24.0
|
||||
github.com/hashicorp/terraform-json v0.27.2
|
||||
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5
|
||||
github.com/onsi/gomega v1.39.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
sigs.k8s.io/controller-runtime v0.23.0
|
||||
k8s.io/api v0.35.2
|
||||
k8s.io/apimachinery v0.35.2
|
||||
k8s.io/client-go v0.35.2
|
||||
sigs.k8s.io/controller-runtime v0.23.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
@@ -66,9 +66,9 @@ require (
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect
|
||||
github.com/fluxcd/pkg/ssh v0.24.0 // indirect
|
||||
github.com/fluxcd/pkg/version v0.12.0 // indirect
|
||||
github.com/fluxcd/pkg/version v0.14.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
@@ -143,12 +143,12 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -78,8 +78,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
|
||||
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
|
||||
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
|
||||
@@ -136,22 +136,22 @@ github.com/fluxcd/notification-controller/api v1.7.5 h1:6CO5bKyjodiK9exQFOdBcz0X
|
||||
github.com/fluxcd/notification-controller/api v1.7.5/go.mod h1:IciwSg8Q0pVtdbsyDyEXx/MxBKWeagxAazpm64C8oCE=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
||||
github.com/fluxcd/pkg/apis/event v0.22.0 h1:nCW0TnneMnscSnj9NlaSKcvyC+436MbY1GyKn/4YnII=
|
||||
github.com/fluxcd/pkg/apis/event v0.22.0/go.mod h1:Hoi4DejaNKVahGkRXqGBjT9h1aKmhc7RCYcsgoTieqc=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0 h1:p8wPIxdmn0vy0a664rsE9JKCfnliZz4HUsDcTy4ZOxA=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0/go.mod h1:XWdsx8P15OiMaQIvmUjYWdmD3zAwhl5q9osl5iCqcOk=
|
||||
github.com/fluxcd/pkg/apis/meta v1.25.0 h1:fmZgMoe7yITGfhFqdOs7w2GOu3Y/2Vvz4+4p/eay3eA=
|
||||
github.com/fluxcd/pkg/apis/meta v1.25.0/go.mod h1:1D92RqAet0/n/cH5S0khBXweirHWkw9rCO0V4NCY6xc=
|
||||
github.com/fluxcd/pkg/git v0.41.0 h1:WvvIUFssFDKpRrptJjDf0B4mrUCwhesv1Txu3DzTsl8=
|
||||
github.com/fluxcd/pkg/git v0.41.0/go.mod h1:iqR4eZEhd3gdRSkv+VDP3Qz9WCner3aZ5ClkOUe+3fc=
|
||||
github.com/fluxcd/pkg/gittestserver v0.24.0 h1:ZIksyENX8yPlB95GJGoUIT171o2oKFJvFSXu+4mEmzU=
|
||||
github.com/fluxcd/pkg/gittestserver v0.24.0/go.mod h1:9l+gwEfqqe/WxiRvIrQxircgDcXUF3/tw/1Bie/XwJc=
|
||||
github.com/fluxcd/pkg/runtime v0.96.0 h1:sF4ic8131BwbOE+T2pkiXlkr2gCaxAho500zlZJJLck=
|
||||
github.com/fluxcd/pkg/runtime v0.96.0/go.mod h1:FyjNMFNAERkCsF/muTWJYU9MZOsq/m4Sc4aQk/EgQ9E=
|
||||
github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE=
|
||||
github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI=
|
||||
github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE=
|
||||
github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0=
|
||||
github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw=
|
||||
github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI=
|
||||
github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8=
|
||||
github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc=
|
||||
github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk=
|
||||
github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw=
|
||||
github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4=
|
||||
github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg=
|
||||
github.com/fluxcd/pkg/version v0.12.0 h1:MGbdbNf2D5wazMqAkNPn+Lh5j+oY0gxQJFTGyet5Hfc=
|
||||
github.com/fluxcd/pkg/version v0.12.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8=
|
||||
github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw=
|
||||
github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8=
|
||||
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
|
||||
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
|
||||
github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b h1:FSPtvaVgL8azcyweqLmD71elAw4vozuXH/QvsJQ7tg0=
|
||||
@@ -168,8 +168,8 @@ github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9n
|
||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -520,27 +520,27 @@ gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
|
||||
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
|
||||
k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
|
||||
k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
|
||||
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
|
||||
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
|
||||
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
|
||||
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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/controller-runtime v0.23.0 h1:Ubi7klJWiwEWqDY+odSVZiFA0aDSevOCXpa38yCSYu8=
|
||||
sigs.k8s.io/controller-runtime v0.23.0/go.mod h1:DBOIr9NsprUqCZ1ZhsuJ0wAnQSIxY/C6VjZbmLgw0j0=
|
||||
sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
|
||||
sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
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/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -94,6 +94,7 @@ spec:
|
||||
t.Logf("failed to delete resources in '%s' namespace: %s", testID, err)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) })
|
||||
|
||||
g.Eventually(func() bool {
|
||||
err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID)
|
||||
|
||||
@@ -143,6 +143,7 @@ metadata:
|
||||
t.Logf("failed to delete resources in '%s' namespace: %s", testID, err)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) })
|
||||
|
||||
g.Eventually(func() bool {
|
||||
err := verifyGitAndKustomization(ctx, testEnv, testID, testID)
|
||||
|
||||
@@ -19,6 +19,7 @@ package integration
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -44,6 +45,7 @@ func TestOCIHelmRelease(t *testing.T) {
|
||||
}
|
||||
g.Expect(testEnv.Create(ctx, &namespace)).To(Succeed())
|
||||
defer testEnv.Delete(ctx, &namespace)
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) })
|
||||
|
||||
repoURL := fmt.Sprintf("%s/charts/podinfo", cfg.testRegistry)
|
||||
err := pushImagesFromURL(repoURL, "ghcr.io/stefanprodan/charts/podinfo:6.2.0", []string{"6.2.0"})
|
||||
@@ -97,23 +99,31 @@ func TestOCIHelmRelease(t *testing.T) {
|
||||
Namespace: helmRelease.Namespace,
|
||||
}
|
||||
if err := testEnv.Get(ctx, nn, chart); err != nil {
|
||||
t.Logf("error getting helm chart %s\n", err.Error())
|
||||
t.Logf("error getting helm chart: %s", err.Error())
|
||||
return false
|
||||
}
|
||||
if err := checkReadyCondition(chart); err != nil {
|
||||
t.Log(err)
|
||||
t.Logf("HelmChart not ready: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
obj := &helmv2.HelmRelease{}
|
||||
nn = types.NamespacedName{Name: helmRelease.Name, Namespace: helmRelease.Namespace}
|
||||
if err := testEnv.Get(ctx, nn, obj); err != nil {
|
||||
t.Logf("error getting helm release %s\n", err.Error())
|
||||
t.Logf("error getting helm release: %s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
if err := checkReadyCondition(obj); err != nil {
|
||||
t.Log(err)
|
||||
// Log all HelmRelease conditions for full picture.
|
||||
var condSummary []string
|
||||
for _, c := range obj.Status.Conditions {
|
||||
condSummary = append(condSummary, fmt.Sprintf("%s=%s (%s)", c.Type, c.Status, c.Message))
|
||||
}
|
||||
t.Logf("HelmRelease not ready: conditions=[%s]", strings.Join(condSummary, "; "))
|
||||
|
||||
// Log pod states in the release namespace.
|
||||
logNamespacePods(t, ctx, testID)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ stringData:
|
||||
log.Printf("failed to delete resources in '%s' namespace", testID)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) })
|
||||
|
||||
if cfg.sopsSecretData != nil {
|
||||
secret := corev1.Secret{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
extgogit "github.com/go-git/go-git/v5"
|
||||
@@ -37,6 +38,9 @@ import (
|
||||
kerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
automationv1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/git"
|
||||
@@ -411,10 +415,164 @@ func checkReadyCondition(from conditions.Getter) error {
|
||||
if conditions.IsReady(from) {
|
||||
return nil
|
||||
}
|
||||
errMsg := fmt.Sprintf("object not ready")
|
||||
errMsg := "object not ready"
|
||||
readyMsg := conditions.GetMessage(from, meta.ReadyCondition)
|
||||
if readyMsg != "" {
|
||||
errMsg += ": " + readyMsg
|
||||
}
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
|
||||
// dumpDiagnostics prints Flux object states and controller logs when a test
|
||||
// has failed. It should be registered via t.Cleanup so that it runs after the
|
||||
// test body completes.
|
||||
func dumpDiagnostics(t *testing.T, ctx context.Context, namespace string) {
|
||||
t.Helper()
|
||||
if !t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("=== Diagnostics dump (test failed) ===")
|
||||
dumpFluxObjects(t, ctx, namespace)
|
||||
dumpControllerLogs(t, ctx)
|
||||
t.Log("=== End diagnostics dump ===")
|
||||
}
|
||||
|
||||
// dumpFluxObjects lists Flux custom resources in the given namespace and prints
|
||||
// their status conditions.
|
||||
func dumpFluxObjects(t *testing.T, ctx context.Context, namespace string) {
|
||||
t.Helper()
|
||||
listOpts := &client.ListOptions{Namespace: namespace}
|
||||
|
||||
gitRepos := &sourcev1.GitRepositoryList{}
|
||||
if err := testEnv.List(ctx, gitRepos, listOpts); err == nil {
|
||||
for _, r := range gitRepos.Items {
|
||||
logObjectStatus(t, "GitRepository", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
helmRepos := &sourcev1.HelmRepositoryList{}
|
||||
if err := testEnv.List(ctx, helmRepos, listOpts); err == nil {
|
||||
for _, r := range helmRepos.Items {
|
||||
logObjectStatus(t, "HelmRepository", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
helmCharts := &sourcev1.HelmChartList{}
|
||||
if err := testEnv.List(ctx, helmCharts, listOpts); err == nil {
|
||||
for _, r := range helmCharts.Items {
|
||||
logObjectStatus(t, "HelmChart", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
kustomizations := &kustomizev1.KustomizationList{}
|
||||
if err := testEnv.List(ctx, kustomizations, listOpts); err == nil {
|
||||
for _, r := range kustomizations.Items {
|
||||
logObjectStatus(t, "Kustomization", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
helmReleases := &helmv2.HelmReleaseList{}
|
||||
if err := testEnv.List(ctx, helmReleases, listOpts); err == nil {
|
||||
for _, r := range helmReleases.Items {
|
||||
logObjectStatus(t, "HelmRelease", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
imageRepos := &reflectorv1.ImageRepositoryList{}
|
||||
if err := testEnv.List(ctx, imageRepos, listOpts); err == nil {
|
||||
for _, r := range imageRepos.Items {
|
||||
logObjectStatus(t, "ImageRepository", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
imagePolicies := &reflectorv1.ImagePolicyList{}
|
||||
if err := testEnv.List(ctx, imagePolicies, listOpts); err == nil {
|
||||
for _, r := range imagePolicies.Items {
|
||||
logObjectStatus(t, "ImagePolicy", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
imageAutomations := &automationv1.ImageUpdateAutomationList{}
|
||||
if err := testEnv.List(ctx, imageAutomations, listOpts); err == nil {
|
||||
for _, r := range imageAutomations.Items {
|
||||
logObjectStatus(t, "ImageUpdateAutomation", r.Name, r.Namespace, r.Status.Conditions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logObjectStatus prints the status conditions of a Flux object.
|
||||
func logObjectStatus(t *testing.T, kind, name, namespace string, conditions []metav1.Condition) {
|
||||
t.Helper()
|
||||
t.Logf(" %s/%s (ns: %s):", kind, name, namespace)
|
||||
for _, c := range conditions {
|
||||
t.Logf(" %s: %s — %s (since %s)", c.Type, c.Status, c.Message, c.LastTransitionTime.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
// dumpControllerLogs prints the logs of all Flux controller pods in the
|
||||
// flux-system namespace.
|
||||
func dumpControllerLogs(t *testing.T, ctx context.Context) {
|
||||
t.Helper()
|
||||
|
||||
podList, err := testEnv.ClientGo.CoreV1().Pods("flux-system").List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
t.Logf("failed to list flux-system pods: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pod := range podList.Items {
|
||||
logs, err := testEnv.ClientGo.
|
||||
CoreV1().
|
||||
Pods(pod.Namespace).
|
||||
GetLogs(pod.Name, &corev1.PodLogOptions{}).
|
||||
DoRaw(ctx)
|
||||
if err != nil {
|
||||
t.Logf("failed to get logs for pod %s: %v", pod.Name, err)
|
||||
continue
|
||||
}
|
||||
t.Logf("=== Logs for pod %s ===\n%s", pod.Name, string(logs))
|
||||
}
|
||||
}
|
||||
|
||||
// logNamespacePods logs the state of all pods in the given namespace,
|
||||
// including container statuses and recent events. Useful for understanding
|
||||
// why a Helm install is stuck.
|
||||
func logNamespacePods(t *testing.T, ctx context.Context, namespace string) {
|
||||
t.Helper()
|
||||
|
||||
podList := &corev1.PodList{}
|
||||
if err := testEnv.List(ctx, podList, &client.ListOptions{Namespace: namespace}); err != nil {
|
||||
t.Logf(" failed to list pods in %s: %v", namespace, err)
|
||||
return
|
||||
}
|
||||
if len(podList.Items) == 0 {
|
||||
t.Logf(" no pods in namespace %s", namespace)
|
||||
return
|
||||
}
|
||||
for _, pod := range podList.Items {
|
||||
t.Logf(" pod %s: phase=%s", pod.Name, pod.Status.Phase)
|
||||
for _, cs := range pod.Status.ContainerStatuses {
|
||||
if cs.State.Waiting != nil {
|
||||
t.Logf(" container %s: waiting — %s: %s", cs.Name, cs.State.Waiting.Reason, cs.State.Waiting.Message)
|
||||
} else if cs.State.Terminated != nil {
|
||||
t.Logf(" container %s: terminated — %s (exit %d)", cs.Name, cs.State.Terminated.Reason, cs.State.Terminated.ExitCode)
|
||||
} else if cs.State.Running != nil {
|
||||
t.Logf(" container %s: running (ready=%v)", cs.Name, cs.Ready)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log recent events in the namespace for scheduling/pull failures.
|
||||
events, err := testEnv.ClientGo.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
t.Logf(" failed to list events in %s: %v", namespace, err)
|
||||
return
|
||||
}
|
||||
if len(events.Items) > 0 {
|
||||
t.Logf(" events in namespace %s:", namespace)
|
||||
for _, e := range events.Items {
|
||||
t.Logf(" %s %s/%s: %s — %s", e.Type, e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Reason, e.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user