1
0
mirror of synced 2026-05-30 03:40:47 +00:00

Compare commits

..

17 Commits

Author SHA1 Message Date
Stefan Prodan ca29bb1a41 Merge pull request #5571 from fluxcd/backport-5570-to-release/v2.7.x
[release/v2.7.x] Disable AUR publishing
2025-10-06 18:47:00 +03:00
Stefan Prodan c707c3ae7b Disable AUR publishing
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
(cherry picked from commit 71a3dad213)
2025-10-06 15:46:17 +00:00
Matheus Pimenta 53552c8816 Merge pull request #5569 from fluxcd/backport-5568-to-release/v2.7.x
[release/v2.7.x] Update toolkit components
2025-10-06 12:56:14 +01:00
fluxcdbot fb8f2508d1 Update toolkit components
- helm-controller to v1.4.1
  https://github.com/fluxcd/helm-controller/blob/v1.4.1/CHANGELOG.md
- source-controller to v1.7.1
  https://github.com/fluxcd/source-controller/blob/v1.7.1/CHANGELOG.md
- notification-controller to v1.7.2
  https://github.com/fluxcd/notification-controller/blob/v1.7.2/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
(cherry picked from commit 4f2d1c3a2a)
2025-10-06 11:55:26 +00:00
Matheus Pimenta de92e95a50 Merge pull request #5564 from fluxcd/backport-5563-to-release/v2.7.x
[release/v2.7.x] Fix `flux migrate -f` not considering kind comments
2025-10-04 16:34:21 +01:00
Matheus Pimenta b398208c7e Fix flux migrate -f not considering kind comments
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
(cherry picked from commit 7c5fb2297c)
2025-10-04 15:33:47 +00:00
Matheus Pimenta 8d0001f5b6 Merge pull request #5561 from fluxcd/backport-5560-to-release/v2.7.x
[release/v2.7.x] Fix `flux migrate -f` command to work with comments
2025-10-03 15:46:16 +01:00
Matheus Pimenta a2ff6c601e Fix migrate -f command to work with comments
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
(cherry picked from commit 83213ce83f)
2025-10-03 14:45:24 +00:00
Stefan Prodan 4accd37c1a Merge pull request #5559 from fluxcd/backport-5558-to-release/v2.7.x
[release/v2.7.x] Improve `flux migrate` for live cluster migrations
2025-10-03 16:53:50 +03:00
Stefan Prodan 742baa62d1 Improve flux migrate for live cluster migrations
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
(cherry picked from commit 0255957dd7)
2025-10-03 13:52:54 +00:00
Stefan Prodan 784a62c6a5 Merge pull request #5557 from fluxcd/backport-5554-to-release/v2.7.x
[release/v2.7.x] Extend `flux migrate` to work with local files
2025-10-03 11:56:56 +03:00
Matheus Pimenta 0bcccb2482 Extend flux migrate to work with local files
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
(cherry picked from commit a9b5be7ff4)
2025-10-03 08:50:00 +00:00
Matheus Pimenta 17dae751a2 Merge pull request #5553 from fluxcd/backport-5551-to-release/v2.7.x
[release/v2.7.x] Fix `flux push artifact` not working with `--provider`
2025-10-01 10:51:22 +01:00
Matheus Pimenta 93a87240b0 Fix flux push artifact not working with --provider
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
(cherry picked from commit 039d79b3c2)
2025-10-01 09:39:54 +00:00
Stefan Prodan 9c66b0d752 Merge pull request #5552 from fluxcd/backport-ci-prs
Backport CI fixes and updates
2025-10-01 11:47:26 +03:00
dependabot[bot] 9fc6853024 build(deps): bump the ci group across 1 directory with 3 updates
Bumps the ci group with 3 updates in the / directory: [docker/login-action](https://github.com/docker/login-action), [ossf/scorecard-action](https://github.com/ossf/scorecard-action) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `docker/login-action` from 3.5.0 to 3.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/184bdaa0721073962dff0199f1fb9940f07167d1...5e57cd118135c172c3672efd75eb46360885c0ef)

Updates `ossf/scorecard-action` from 2.4.2 to 2.4.3
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/05b42c624433fc40578a4040d5cf5e36ddca8cde...4eaacf0543bb3f2c246792bd56e8cdeffafb205a)

Updates `github/codeql-action` from 3.30.3 to 3.30.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/192325c86100d080feab897ff886c34abd4c83a3...3599b3baa15b485a2e49ef411a7a4bb2452e7f93)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci
- dependency-name: github/codeql-action
  dependency-version: 3.30.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 09:35:06 +01:00
Stefan Prodan 22a8310b0a ci: Set GITHUB_TOKEN in the release-flux-manifests workflow
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2025-10-01 09:34:46 +01:00
183 changed files with 1476 additions and 9471 deletions
+6 -6
View File
@@ -44,12 +44,12 @@
description: Feature request proposals in the RFC format description: Feature request proposals in the RFC format
color: '#D621C3' color: '#D621C3'
aliases: ['area/RFC'] aliases: ['area/RFC']
- name: backport:release/v2.4.x
description: To be backported to release/v2.4.x
color: '#ffd700'
- name: backport:release/v2.5.x
description: To be backported to release/v2.5.x
color: '#ffd700'
- name: backport:release/v2.6.x - name: backport:release/v2.6.x
description: To be backported to release/v2.6.x description: To be backported to release/v2.6.x
color: '#ffd700' 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'
+1 -1
View File
@@ -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`. - Updates the controller API package version in `go.mod`.
- Patches the controller CRDs version in the `manifests/crds` overlay. - Patches the controller CRDs version in the `manifests/crds` overlay.
- Patches the controller Deployment version in `manifests/bases` overlay. - Patches the controller Deployment version in `manifests/bases` overlay.
- Opens a Pull Request against the checked out branch. - Opens a Pull Request against the `main` branch.
- Triggers the e2e test suite to run for the opened PR. - Triggers the e2e test suite to run for the opened PR.
+1 -1
View File
@@ -24,6 +24,6 @@ jobs:
name: action on ${{ matrix.version }} name: action on ${{ matrix.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup flux - name: Setup flux
uses: ./action uses: ./action
+2 -2
View File
@@ -8,6 +8,6 @@ jobs:
permissions: permissions:
contents: write # for reading and creating branches. contents: write # for reading and creating branches.
pull-requests: write # for creating pull requests against release branches. pull-requests: write # for creating pull requests against release branches.
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0 uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.4.0
secrets: secrets:
github-token: ${{ secrets.BOT_GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
+21 -19
View File
@@ -3,13 +3,13 @@ name: conformance
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ] branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
permissions: permissions:
contents: read contents: read
env: env:
GO_VERSION: 1.26.x GO_VERSION: 1.25.x
jobs: jobs:
conform-kubernetes: conform-kubernetes:
@@ -19,13 +19,13 @@ jobs:
matrix: matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes # Keep this list up-to-date with https://endoflife.date/kubernetes
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml # Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
KUBERNETES_VERSION: [1.33.0, 1.34.1, 1.35.0] KUBERNETES_VERSION: [1.32.1, 1.33.0, 1.34.1]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
cache-dependency-path: | cache-dependency-path: |
@@ -40,7 +40,7 @@ jobs:
run: | run: |
make build make build
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
with: with:
version: v0.30.0 version: v0.30.0
cluster_name: ${{ steps.prep.outputs.CLUSTER }} cluster_name: ${{ steps.prep.outputs.CLUSTER }}
@@ -76,13 +76,13 @@ jobs:
matrix: matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes # Keep this list up-to-date with https://endoflife.date/kubernetes
# Available versions can be found with "replicated cluster versions" # Available versions can be found with "replicated cluster versions"
K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ] K3S_VERSION: [ 1.32.8, 1.33.4 ]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
cache-dependency-path: | cache-dependency-path: |
@@ -97,7 +97,7 @@ jobs:
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml" KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Build - name: Build
run: make build-dev run: make build-dev
- name: Create repository - name: Create repository
@@ -107,7 +107,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Create cluster - name: Create cluster
id: create-cluster id: create-cluster
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: "k3s" kubernetes-distribution: "k3s"
@@ -120,7 +120,8 @@ jobs:
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
- name: Run flux bootstrap - name: Run flux bootstrap
run: | run: |
./bin/flux bootstrap git --manifests ./manifests/test/ \ ./bin/flux bootstrap git --manifests ./manifests/install/ \
--components-extra=image-reflector-controller,image-automation-controller \
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \ --url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
--branch=main \ --branch=main \
--path=clusters/k3s \ --path=clusters/k3s \
@@ -150,7 +151,7 @@ jobs:
kubectl delete ns flux-system --wait kubectl delete ns flux-system --wait
- name: Delete cluster - name: Delete cluster
if: ${{ always() }} if: ${{ always() }}
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
continue-on-error: true continue-on-error: true
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}
@@ -168,13 +169,13 @@ jobs:
strategy: strategy:
matrix: matrix:
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift # Keep this list up-to-date with https://endoflife.date/red-hat-openshift
OPENSHIFT_VERSION: [ 4.20.0-okd ] OPENSHIFT_VERSION: [ 4.19.0-okd ]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
cache-dependency-path: | cache-dependency-path: |
@@ -189,7 +190,7 @@ jobs:
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml" KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Build - name: Build
run: make build-dev run: make build-dev
- name: Create repository - name: Create repository
@@ -199,7 +200,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Create cluster - name: Create cluster
id: create-cluster id: create-cluster
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: "openshift" kubernetes-distribution: "openshift"
@@ -211,6 +212,7 @@ jobs:
- name: Run flux bootstrap - name: Run flux bootstrap
run: | run: |
./bin/flux bootstrap git --manifests ./manifests/openshift/ \ ./bin/flux bootstrap git --manifests ./manifests/openshift/ \
--components-extra=image-reflector-controller,image-automation-controller \
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \ --url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
--branch=main \ --branch=main \
--path=clusters/openshift \ --path=clusters/openshift \
@@ -240,7 +242,7 @@ jobs:
kubectl delete ns flux-system --wait kubectl delete ns flux-system --wait
- name: Delete cluster - name: Delete cluster
if: ${{ always() }} if: ${{ always() }}
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
continue-on-error: true continue-on-error: true
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}
+4 -4
View File
@@ -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]' if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: CheckoutD - name: CheckoutD
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: 1.26.x go-version: 1.25.x
cache-dependency-path: tests/integration/go.sum cache-dependency-path: tests/integration/go.sum
- name: Setup Terraform - name: Setup Terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
- name: Setup Flux CLI - name: Setup Flux CLI
run: make build run: make build
working-directory: ./ working-directory: ./
+12 -13
View File
@@ -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]' if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: 1.26.x go-version: 1.25.x
cache-dependency-path: | cache-dependency-path: |
**/go.sum **/go.sum
**/go.mod **/go.mod
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
with: with:
version: v0.30.0 version: v0.30.0
cluster_name: kind cluster_name: kind
# The versions below should target the newest Kubernetes version # The versions below should target the newest Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes # Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64 node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
kubectl_version: v1.33.0 kubectl_version: v1.32.0
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Setup yq - name: Setup yq
uses: fluxcd/pkg/actions/yq@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/yq@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Build - name: Build
run: make build-dev run: make build-dev
- name: Set outputs - name: Set outputs
@@ -51,7 +51,7 @@ jobs:
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
- name: bootstrap init - name: bootstrap init
run: | run: |
./bin/flux bootstrap github --manifests ./manifests/test/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \ --image-pull-secret=ghcr-auth \
--registry-creds=fluxcd:$GITHUB_TOKEN \ --registry-creds=fluxcd:$GITHUB_TOKEN \
@@ -66,7 +66,7 @@ jobs:
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
- name: bootstrap no-op - name: bootstrap no-op
run: | run: |
./bin/flux bootstrap github --manifests ./manifests/test/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \ --image-pull-secret=ghcr-auth \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
@@ -78,7 +78,7 @@ jobs:
- name: bootstrap customize - name: bootstrap customize
run: | run: |
make setup-bootstrap-patch make setup-bootstrap-patch
./bin/flux bootstrap github --manifests ./manifests/test/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \ --branch=main \
@@ -98,14 +98,13 @@ jobs:
- name: test image automation - name: test image automation
run: | run: |
make setup-image-automation make setup-image-automation
./bin/flux bootstrap github --manifests ./manifests/test/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \ --branch=main \
--path=test-cluster \ --path=test-cluster \
--read-write-key --read-write-key
./bin/flux reconcile image repository podinfo ./bin/flux reconcile image repository podinfo
./bin/flux reconcile image policy podinfo
./bin/flux reconcile image update flux-system ./bin/flux reconcile image update flux-system
./bin/flux get images all ./bin/flux get images all
./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system ./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system
+7 -7
View File
@@ -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]' if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: 1.26.x go-version: 1.25.x
cache-dependency-path: tests/integration/go.sum cache-dependency-path: tests/integration/go.sum
- name: Setup Terraform - name: Setup Terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
- name: Setup Flux CLI - name: Setup Flux CLI
run: make build run: make build
working-directory: ./ working-directory: ./
@@ -56,11 +56,11 @@ jobs:
- name: Setup gcloud - name: Setup gcloud
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log into us-central1-docker.pkg.dev - name: Log into us-central1-docker.pkg.dev
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: us-central1-docker.pkg.dev registry: us-central1-docker.pkg.dev
username: oauth2accesstoken username: oauth2accesstoken
+8 -8
View File
@@ -23,16 +23,16 @@ jobs:
- 5000:5000 - 5000:5000
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: 1.26.x go-version: 1.25.x
cache-dependency-path: | cache-dependency-path: |
**/go.sum **/go.sum
**/go.mod **/go.mod
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
with: with:
version: v0.30.0 version: v0.30.0
cluster_name: kind cluster_name: kind
@@ -40,13 +40,13 @@ jobs:
config: .github/kind/config.yaml # disable KIND-net config: .github/kind/config.yaml # disable KIND-net
# The versions below should target the oldest supported Kubernetes version # The versions below should target the oldest supported Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes # Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64 node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
kubectl_version: v1.33.0 kubectl_version: v1.32.0
- name: Setup Calico for network policy - name: Setup Calico for network policy
run: | run: |
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Run tests - name: Run tests
run: make test run: make test
- name: Run e2e tests - name: Run e2e tests
@@ -65,7 +65,7 @@ jobs:
./bin/flux check --pre ./bin/flux check --pre
- name: flux install --manifests - name: flux install --manifests
run: | run: |
./bin/flux install --manifests ./manifests/test/ ./bin/flux install --manifests ./manifests/install/
- name: flux create secret - name: flux create secret
run: | run: |
./bin/flux create secret git git-ssh-test \ ./bin/flux create secret git git-ssh-test \
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
actions: read actions: read
contents: read contents: read
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Run analysis - name: Run analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with: with:
@@ -28,12 +28,12 @@ jobs:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
publish_results: true publish_results: true
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
retention-days: 5 retention-days: 5
- name: Upload SARIF results - name: Upload SARIF results
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+18 -24
View File
@@ -13,44 +13,40 @@ jobs:
hashes: ${{ steps.slsa.outputs.hashes }} hashes: ${{ steps.slsa.outputs.hashes }}
image_url: ${{ steps.slsa.outputs.image_url }} image_url: ${{ steps.slsa.outputs.image_url }}
image_digest: ${{ steps.slsa.outputs.image_digest }} image_digest: ${{ steps.slsa.outputs.image_digest }}
runs-on: runs-on: ubuntu-latest
group: "Default Larger Runners"
labels: ubuntu-latest-16-cores
permissions: permissions:
contents: write # needed to write releases contents: write # needed to write releases
id-token: write # needed for keyless signing id-token: write # needed for keyless signing
packages: write # needed for ghcr access packages: write # needed for ghcr access
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Unshallow - name: Unshallow
run: git fetch --prune --unshallow run: git fetch --prune --unshallow
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: 1.26.x go-version: 1.25.x
cache: false cache: false
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Setup Syft - name: Setup Syft
uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1 uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
- name: Setup Cosign - name: Setup Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@@ -63,7 +59,7 @@ jobs:
run: | run: |
kustomize build manifests/crds > all-crds.yaml kustomize build manifests/crds > all-crds.yaml
- name: Generate OpenAPI JSON schemas from CRDs - name: Generate OpenAPI JSON schemas from CRDs
uses: fluxcd/pkg/actions/crdjsonschema@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/crdjsonschema@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
with: with:
crd: all-crds.yaml crd: all-crds.yaml
output: schemas output: schemas
@@ -72,7 +68,7 @@ jobs:
tar -czvf ./output/crd-schemas.tar.gz -C schemas . tar -czvf ./output/crd-schemas.tar.gz -C schemas .
- name: Run GoReleaser - name: Run GoReleaser
id: run-goreleaser id: run-goreleaser
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with: with:
version: latest version: latest
args: release --skip=validate args: release --skip=validate
@@ -103,9 +99,9 @@ jobs:
id-token: write id-token: write
packages: write packages: write
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
- name: Setup Flux CLI - name: Setup Flux CLI
uses: ./action/ uses: ./action/
with: with:
@@ -116,13 +112,13 @@ jobs:
VERSION=$(flux version --client | awk '{ print $NF }') VERSION=$(flux version --client | awk '{ print $NF }')
echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
registry: ghcr.io registry: ghcr.io
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@@ -150,9 +146,7 @@ jobs:
--path="./flux-system" \ --path="./flux-system" \
--source=${{ github.repositoryUrl }} \ --source=${{ github.repositoryUrl }} \
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}" --revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
- uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
- name: Sign manifests - name: Sign manifests
env: env:
COSIGN_EXPERIMENTAL: 1 COSIGN_EXPERIMENTAL: 1
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
permissions: permissions:
contents: read # for reading the repository code. contents: read # for reading the repository code.
security-events: write # for uploading the CodeQL analysis results. security-events: write # for uploading the CodeQL analysis results.
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0 uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.4.0
secrets: secrets:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
fossa-token: ${{ secrets.FOSSA_TOKEN }} fossa-token: ${{ secrets.FOSSA_TOKEN }}
+1 -1
View File
@@ -12,6 +12,6 @@ jobs:
permissions: permissions:
contents: read # for reading the labels file. contents: read # for reading the labels file.
issues: write # for creating and updating labels. issues: write # for creating and updating labels.
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0 uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.4.0
secrets: secrets:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
+5 -5
View File
@@ -16,11 +16,11 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: 1.26.x go-version: 1.25.x
cache-dependency-path: | cache-dependency-path: |
**/go.sum **/go.sum
**/go.mod **/go.mod
@@ -96,7 +96,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
commit-message: | commit-message: |
@@ -106,7 +106,7 @@ jobs:
committer: GitHub <noreply@github.com> committer: GitHub <noreply@github.com>
author: fluxcdbot <fluxcdbot@users.noreply.github.com> author: fluxcdbot <fluxcdbot@users.noreply.github.com>
signoff: true signoff: true
branch: update-components-${{ github.ref_name }} branch: update-components
title: Update toolkit components title: Update toolkit components
body: | body: |
${{ steps.update.outputs.pr_body }} ${{ steps.update.outputs.pr_body }}
-13
View File
@@ -1,13 +0,0 @@
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 }}
-151
View File
@@ -1,151 +0,0 @@
# AGENTS.md
Guidance for AI coding assistants working in `fluxcd/flux2`. Read this file before making changes.
## Contribution workflow for AI agents
These rules come from [`fluxcd/flux2/CONTRIBUTING.md`](https://github.com/fluxcd/flux2/blob/main/CONTRIBUTING.md) and apply to every Flux repository.
- **Do not add `Signed-off-by` or `Co-authored-by` trailers with your agent name.** Only a human can legally certify the DCO.
- **Disclose AI assistance** with an `Assisted-by` trailer naming your agent and model:
```sh
git commit -s -m "Add support for X" --trailer "Assisted-by: <agent-name>/<model-id>"
```
The `-s` flag adds the human's `Signed-off-by` from their git config — do not remove it.
- **Commit message format:** Subject in imperative mood ("Add feature X" instead of "Adding feature X"), capitalized, no trailing period, ≤50 characters. Body wrapped at 72 columns, explaining what and why. No `@mentions` or `#123` issue references in the commit — put those in the PR description.
- **Trim verbiage:** in PR descriptions, commit messages, and code comments. No marketing prose, no restating the diff, no emojis.
- **Rebase, don't merge:** Never merge `main` into the feature branch; rebase onto the latest `main` and push with `--force-with-lease`. Squash before merge when asked.
- **Pre-PR gate:** `make tidy fmt vet && make test` must pass and the working tree must be clean.
- **Flux is GA:** Backward compatibility is mandatory. Breaking changes to CLI flags, output format, or behavior will be rejected. Design additive changes.
- **Copyright:** All new `.go` files must begin with the header from `cmd/flux/main.go` (Apache 2.0). Update the year to the current year when copying.
- **Tests:** New features, improvements and fixes must have test coverage. Add unit tests in `cmd/flux/*_test.go` tagged `//go:build unit`. Follow the existing `cmdTestCase` + golden file patterns. Run tests locally before pushing.
## Code quality
Before submitting code, review your changes for the following:
- **No secrets in logs or output.** Never surface auth tokens, passwords, deploy keys, or credential URLs in error messages, log lines, or CLI output. Bootstrap and source-secret commands handle sensitive material — take extra care.
- **No unchecked I/O.** Close HTTP response bodies, file handles, and tar readers in `defer` statements. Check and propagate errors from I/O operations.
- **No path traversal.** Validate and sanitize file paths extracted from archives or user input. Never `filepath.Join` with untrusted components without validation.
- **No command injection.** Do not shell out via `os/exec` for git, helm, or kustomize operations. Use the Go libraries already in use (`fluxcd/pkg/git`, `fluxcd/pkg/kustomize`, `fluxcd/pkg/ssa`).
- **No hardcoded defaults for security settings.** TLS verification must remain enabled by default. Git auth settings come from user-provided secrets.
- **Error handling.** Wrap errors with `%w` for chain inspection. Do not swallow errors silently. CLI errors must be actionable — tell the user what went wrong and how to fix it without leaking internal state.
- **Resource cleanup.** Ensure temporary files and directories (manifest staging, downloaded tarballs) are cleaned up on all code paths (success and error). Use `defer` and `t.TempDir()` in tests.
- **No panics.** Never use `panic` in runtime code paths. Return errors and let the CLI handle them gracefully.
- **Output discipline.** Machine-readable data (tables, YAML, JSON) goes to stdout via `rootCmd.OutOrStdout()`. Human-readable status messages go to stderr via the `stderrLogger`.
- **Minimal surface.** Keep new exported APIs in `pkg/` to the minimum needed. Every export is a backward-compatibility commitment.
## Project overview
flux2 is the Flux CLI (`flux` command) and distribution repository. It is **not** a controller — it consumes CRD APIs from six independent controller repos (source-controller, kustomize-controller, helm-controller, notification-controller, image-reflector-controller, image-automation-controller). It serves two purposes:
1. **CLI tool** — a Cobra-based binary that installs Flux onto Kubernetes clusters, bootstraps GitOps pipelines, and manages all Flux CRD objects (create, get, export, reconcile, suspend, resume, delete, diff, build, etc.).
2. **Distribution hub** — it bundles the Kustomize manifests for all Flux controllers and releases them as `manifests.tar.gz` on GitHub. Those manifests are also compiled into the binary itself via `//go:embed`.
## Repository layout
- `cmd/flux/` — all CLI source. Single `main` package with one file per command or resource type. `main.go` defines the root cobra command with global flags. `manifests.embed.go` embeds the generated controller manifests via `//go:embed`.
- `internal/build/` — `flux build kustomization` logic (kustomize-based diff/build, SOPS secret masking).
- `internal/flags/` — custom `pflag.Value` types providing enum validation (e.g. `LogLevel`, `ECDSACurve`, `RSAKeyBits`, `PublicKeyAlgorithm`, `DecryptionProvider`).
- `internal/tree/` — tree-printing helper for `flux tree kustomization`.
- `internal/utils/` — shared helpers: `KubeClient`, `KubeConfig`, `NewScheme` (registers all controller API groups), `Apply` (SSA-based two-phase apply), `ExecKubectlCommand`, `ValidateComponents`.
- `pkg/bootstrap/` — bootstrap orchestration: `Run()`, `PlainGitBootstrapper`, `ProviderBootstrapper`. `provider/` has the git provider factory (GitHub, GitLab, Gitea, Bitbucket).
- `pkg/log/` — `Logger` interface (`Actionf`, `Generatef`, `Waitingf`, `Successf`, `Warningf`, `Failuref`).
- `pkg/manifestgen/` — manifest generation for install, sync, kustomization, and source secrets.
- `pkg/printers/` — specialized printers `TablePrinter` and `DyffPrinter`.
- `pkg/status/` — `StatusChecker` using `fluxcd/cli-utils` kstatus polling.
- `pkg/uninstall/` — `flux uninstall` logic.
- `manifests/` — Kustomize bases per controller, RBAC, network policies, CRD references, and `scripts/bundle.sh` which runs `kustomize build` to generate `cmd/flux/manifests/`.
- `tests/integration/` — cloud e2e tests (Azure/GCP) with their own `go.mod` and Terraform infrastructure.
- `rfcs/` — Request for Comments documents for major design proposals and changes.
## CLI architecture
Commands are Cobra-based, organized as parent + per-resource children:
- Group files (`create.go`, `get.go`, `reconcile.go`, etc.) register the parent subcommand.
- Per-resource files (`create_kustomization.go`, `get_helmrelease.go`, etc.) register children.
Core interfaces in `cmd/flux/` enable generic command implementations:
- `adapter` / `copyable` / `listAdapter` — wrap controller API types for generic CRUD.
- `reconcilable` — annotate-and-poll pattern for triggering reconciliation.
- `summarisable` — generic table output for `get` commands.
Each resource type (e.g. `kustomizationAdapter` in `kustomization.go`) wraps the controller API type and implements these interfaces. Follow this pattern when adding new resource support.
Commands interact with the Kubernetes API via `internal/utils.KubeClient()` → `client.WithWatch`. `internal/utils.NewScheme()` registers all six controller API groups plus core k8s types. `internal/utils.Apply()` implements SSA-based two-phase apply (CRDs/Namespaces first, then remaining objects).
## Manifest pipeline
1. `manifests/bases/<controller>/` contains a Kustomize base per controller referencing the controller's GitHub release for CRDs and deployment manifests.
2. `manifests/install/kustomization.yaml` assembles all bases plus RBAC and policies.
3. `manifests/scripts/bundle.sh` runs `kustomize build` on each base, writing output to `cmd/flux/manifests/` (not checked in — generated).
4. The Makefile `$(EMBEDDED_MANIFESTS_TARGET)` runs `bundle.sh` and creates a sentinel file `cmd/flux/.manifests.done`.
5. `cmd/flux/manifests.embed.go` uses `//go:embed manifests/*.yaml` to compile everything into the binary.
When modifying `manifests/`, always run `make build` and verify the generated output before committing. Never hand-edit files under `cmd/flux/manifests/`.
## Build, test, lint
All targets in the root `Makefile`. Go version tracks `go.mod`.
- `make tidy` — tidy the root module and `tests/integration/`.
- `make fmt` / `make vet` — run in the root module.
- `make build` — builds `bin/flux` (CGO disabled, version injected via ldflags). Depends on embedded manifests being generated.
- `make build-dev` — builds with `DEV_VERSION`.
- `make install` / `make install-dev` — `go install` or copy to `/usr/local/bin`.
- `make test` — unit tests with envtest: runs `tidy fmt vet install-envtest`, then `go test ./... -coverprofile cover.out --tags=unit $(TEST_ARGS)`.
- `make e2e` — e2e tests against a live cluster: `go test ./cmd/flux/... --tags=e2e -v -failfast`.
- `make test-with-kind` — sets up a kind cluster, runs e2e, tears it down.
- `make install-envtest` — downloads `setup-envtest` and fetches k8s binaries into `testbin/`.
Run a single test: `make test TEST_ARGS='-run TestCreate -v'`.
## Codegen and generated files
Check `go.mod` and the `Makefile` for current dependency and tool versions. The main codegen pipeline is the manifest bundle:
```sh
./manifests/scripts/bundle.sh
```
Generated files (never hand-edit):
- `cmd/flux/manifests/*.yaml` — generated by `bundle.sh` from `manifests/` sources.
- `cmd/flux/.manifests.done` — sentinel file tracking bundle state.
Bump `fluxcd/pkg/*` and controller `api` modules as a set. Run `make tidy` after any bump.
## Conventions
- Standard `gofmt`. All exported names need doc comments.
- **Command pattern:** follow the existing group-parent + per-resource-child cobra structure. New resources need an adapter type implementing `adapter`, `copyable`, and the relevant command interfaces (`reconcilable`, `summarisable`, etc.).
- **Output:** stderr for human status messages via `stderrLogger` (Unicode symbols: `` action, `` success, `` failure, `` waiting, `⚠️` warning, `` generate). Stdout for machine-readable data (tables, YAML, JSON) via `rootCmd.OutOrStdout()`.
- **Global flags:** kubeconfig flags come from `k8s.io/cli-runtime/pkg/genericclioptions.ConfigFlags`. Client tuning comes from `fluxcd/pkg/runtime/client.Options`. `FLUX_SYSTEM_NAMESPACE` env var overrides the default namespace.
- **SSA apply:** always use `internal/utils.Apply()` (two-phase: CRDs/Namespaces first, then rest). Do not apply manifests directly via the k8s client.
- **Reconcile triggering:** patch `meta.ReconcileRequestAnnotation` with a timestamp, then poll with `kstatus.Compute()` until ready. See `reconcile.go`.
- **Error handling:** return errors from `RunE`. Use `*RequestError` with exit codes for actionable CLI errors. Exit code 1 = warning, anything else = failure.
- **Flags:** use `internal/flags/` custom `pflag.Value` types for enum flags (providers, algorithms, sources). Add new enum types there.
## Testing
Three test suites with build tags:
- **Unit** (`//go:build unit`): lives in `cmd/flux/*_test.go`. Uses `controller-runtime/envtest` for an in-process fake k8s API. CRDs are loaded from `cmd/flux/manifests/` (embedded manifests). Pattern: `cmdTestCase{args: "...", assert: assertGoldenFile("testdata/...")}`. The `executeCommand()` helper captures stdout.
- **E2e** (`//go:build e2e`): lives in `cmd/flux/*_test.go`. Requires a live cluster via `TEST_KUBECONFIG`. `TestMain` runs `flux install` for setup and teardown.
- **Integration** (`//go:build integration`): lives in `tests/integration/` with its own `go.mod`. Uses Terraform-provisioned cloud clusters.
Golden files live in `cmd/flux/testdata/`. Update them with `go test ./cmd/flux/... --tags=unit -update`.
Run a single unit test: `make test TEST_ARGS='-run TestInstall -v'`.
## Gotchas and non-obvious rules
- The `cmd/flux/manifests/` directory is **generated, not checked in**. It is created by `manifests/scripts/bundle.sh` and embedded into the binary. `make build` and `make test` both trigger the bundle if the sentinel file is stale.
- `kustomize` must be on `PATH` for `bundle.sh` to work. If you see "command not found" errors during build, install kustomize.
- `internal/utils.NewScheme()` registers all six controller API groups. Adding support for a new CRD type means updating the scheme registration there.
- The `VERSION` constant is injected via `-ldflags` at build time. In dev builds it defaults to `0.0.0-dev.0`. The embedded manifest version check (`isEmbeddedVersion`) determines whether `flux install` uses compiled-in manifests or downloads from GitHub.
- `resetCmdArgs()` in tests is critical — Cobra persists flag state between test runs. Every test case must reset to avoid pollution.
- `executeCommand()` captures stdout only. Stderr output (from `stderrLogger`) is not captured in test assertions. If your command's output goes to the wrong stream, tests will silently pass with empty golden files.
- The `adapter` / `listAdapter` interfaces use type assertions internally. If you add a new resource type and forget to implement an interface method, you'll get a runtime panic in the generic command handler, not a compile error. Add interface compliance checks (`var _ reconcilable = ...`).
- Bootstrap commands create real Git commits and push to real repos. E2e tests for bootstrap need careful cleanup. Do not add bootstrap e2e tests without a corresponding teardown.
- `pkg/` packages are importable by external consumers (e.g. Terraform provider, other tools). Treat their exported surface as public API.
+130 -105
View File
@@ -1,129 +1,154 @@
# Contributing # Contributing
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and accepts contributions via GitHub pull requests. Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and
This document outlines the conventions to get your contribution accepted. accepts contributions via GitHub pull requests. This document outlines
We gratefully welcome improvements to documentation as well as code contributions. some of the conventions on to make it easier to get your contribution
accepted.
If you are new to the project, we recommend starting with documentation improvements or We gratefully welcome improvements to issues and documentation as well as to
small bug fixes to get familiar with the codebase and the contribution process. code.
## Project Structure
The Flux project consists of a set of Kubernetes controllers and tools that implement the GitOps pattern.
The main repositories in the Flux project are:
- [fluxcd/flux2](https://github.com/fluxcd/flux2): The Flux distribution and command-line interface (CLI)
- [fluxcd/pkg](https://github.com/fluxcd/pkg): The GitOps Toolkit Go SDK for building Flux controllers and CLI plugins
- [fluxcd/source-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets)
- [fluxcd/source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns
- [fluxcd/kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
- [fluxcd/helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for lifecycle management of Helm releases
- [fluxcd/notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events (alerts and webhook receivers)
- [fluxcd/image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries for new image tags and digests
- [fluxcd/image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patching container image tags and digests in Git repositories
- [fluxcd/website](https://github.com/fluxcd/website): The Flux documentation website accessible at <https://fluxcd.io/>
## AI Coding Assistants Guidance
Using AI Agents to help write your PR is acceptable, but as the author, you are responsible
for understanding the code and the documentation you submit. Please review all the AI-generated
content and make sure it follows the guidelines in this document before submitting your PR.
All Flux repositories contain an `AGENTS.md` file. You must point your AI Agent to
`AGENTS.md` and ask it to follow the guidelines and conventions described there.
Trim down the verbiage in the PR description, commit messages and code comments.
When engaging with Flux maintainers please refrain from using AI Agents to
generate responses, we want to talk to you, not to your AI Agent.
AI Agents **must not** add `Signed-off-by` or `Co-authored-by` tags to the commit message.
Only humans can legally certify the Developer Certificate of Origin ([DCO](https://developercertificate.org/)).
You should disclose the use of AI Agents in the description of your PR and
in the commit message using the `Assisted-by: AGENT_NAME/LLM_VERSION` tag.
Adding the `Assisted-by` tag to the commit message can be done with:
```sh
git commit -s -m "Your commit message" --trailer "Assisted-by: <agent>/<model>"
```
**Note** that the `Signed-off-by` tag is set via the `-s` flag using your real name and email
(`user.name` and `user.email` must be set in Git config).
Example of a commit message disclosing the use of AI assistance:
```text
Add version info to plugin listing
Add a version column to the `flux plugin list` table output and populate
it with the semantic version info extracted from the plugin's recipe file.
For plugins installed via symlinks, the version is set to `unknown`.
Signed-off-by: Jane Doe <jane.doe@example.com>
Assisted-by: copilot/gpt-5.4
```
## Certificate of Origin ## Certificate of Origin
By contributing to this project you agree to the Developer Certificate of Origin (DCO). By contributing to this project you agree to the Developer Certificate of
This document was created by the Linux Kernel community and is a simple statement that you, Origin (DCO). This document was created by the Linux Kernel community and is a
as a contributor, have the legal right to make the contribution. simple statement that you, as a contributor, have the legal right to make the
contribution.
We require all commits to be signed. By signing off with your signature, you certify that you wrote We require all commits to be signed. By signing off with your signature, you
the patch or otherwise have the right to contribute the material by the rules of the [DCO](https://raw.githubusercontent.com/fluxcd/flux2/refs/heads/main/DCO): certify that you wrote the patch or otherwise have the right to contribute the
material by the rules of the [DCO](DCO):
`Signed-off-by: Jane Doe <jane.doe@example.com>` `Signed-off-by: Jane Doe <jane.doe@example.com>`
The signature must contain your real name (sorry, no pseudonyms or anonymous contributions). The signature must contain your real name
If your `user.name` and `user.email` are set in your Git config, (sorry, no pseudonyms or anonymous contributions)
If your `user.name` and `user.email` are configured in your Git config,
you can sign your commit automatically with `git commit -s`. you can sign your commit automatically with `git commit -s`.
## Communications
For realtime communications we use Slack: To join the conversation, simply
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the
[#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
To discuss ideas and specifications we use [Github
Discussions](https://github.com/fluxcd/flux2/discussions).
For announcements we use a mailing list as well. Simply subscribe to
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev)
to join the conversation (there you can also add calendar invites
to your Google calendar for our [Flux
meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)).
## Understanding Flux and the GitOps Toolkit
If you are entirely new to Flux and the GitOps Toolkit,
you might want to take a look at the [introductory talk and demo](https://www.youtube.com/watch?v=qQBtSkgl7tI).
This project is composed of:
- [flux2](https://github.com/fluxcd/flux2): The Flux CLI
- [source-manager](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git and Helm repositories, S3-compatible Buckets)
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
### Understanding the code
To get started with developing controllers, you might want to review
[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
walks you through writing a short and concise controller that watches out
for source changes.
## How to run the test suite
Prerequisites:
* go >= 1.24
* kubectl >= 1.30
* kustomize >= 5.0
* coreutils (on Mac OS)
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
```bash
make install-envtest
```
Then you can run the unit tests with:
```bash
make test
```
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
create a cluster for testing with:
```bash
make setup-kind
```
Then you can run the end-to-end tests with:
```bash
make e2e
```
When the output of the Flux CLI changes, to automatically update the golden
files used in the test, pass `-update` flag to the test as:
```bash
make e2e TEST_ARGS="-update"
```
Since not all packages use golden files for testing, `-update` argument must be
passed only for the packages that use golden files. Use the variables
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
path of the target test package:
```bash
# Unit test
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
# e2e test
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
```
Teardown the e2e environment with:
```bash
make cleanup-kind
```
## Acceptance policy ## Acceptance policy
These things will make a PR more likely to be accepted: These things will make a PR more likely to be accepted:
- Addressing an open issue, if one doesn't exist, please open an issue to discuss the problem and the proposed solution before submitting a PR. - a well-described requirement
- Flux is GA software and we are committed to maintaining backward compatibility. If your contribution introduces a breaking change, expect for your PR to be rejected. - tests for new code
- New code and tests must follow the conventions in the existing code and tests. All new code must have good test coverage and be well documented. - tests for old code!
- All top-level Go code and exported names should have doc comments, as should non-trivial unexported type or function declarations. - new code and tests follow the conventions in old code and tests
- Before submitting a PR, make sure that your code is properly formatted by running `make tidy fmt vet` and that all tests are passing by running `make test`. - a good commit message (see below)
- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1)
- code must build on both Linux and Darwin, via plain `go build`
- code should have appropriate test coverage and tests should be written
to work with `go test`
In general, we will merge a PR once one maintainer has endorsed it. In general, we will merge a PR once one maintainer has endorsed it.
For substantial changes, more people may become involved, and you might For substantial changes, more people may become involved, and you might
get asked to resubmit the PR or divide the changes into more than one PR. get asked to resubmit the PR or divide the changes into more than one PR.
## Format of the Commit Message ### Format of the Commit Message
For the Flux project we prefer the following rules: For the GitOps Toolkit controllers we prefer the following rules for good commit messages:
- Limit the subject to 50 characters, start with a capital letter and do not end with a period. - Limit the subject to 50 characters and write as the continuation
- Explain what and why in the body, if more than a trivial change; wrap it at 72 characters. of the sentence "If applied, this commit will ..."
- Use the imperative mood in the subject line (e.g., "Add support for X" instead of "Added support for X" or "Adds support for X"). - Explain what and why in the body, if more than a trivial change;
- Do not include GitHub mentions to issues in the commit message, use the PR description instead (e.g., "Fixes #123" or "Closes #123"). wrap it at 72 characters.
- Do not include GitHub mentions to accounts (e.g., `@username` or `@team`) within the commit message.
## Pull Request Process The [following article](https://chris.beams.io/posts/git-commit/#seven-rules)
has some more helpful advice on documenting your work.
Fork the repository and create a new branch for your changes, do not commit directly to the `main` branch.
Once you have made your changes and committed them, push your branch to your fork and open a pull request
against the `main` branch of the Flux repository.
During the review process, you may be asked to make changes to your PR. Add commits to address the feedback
without force pushing, as this will make it easier for reviewers to see the changes.
Before committing, make sure to run `make test` to ensure that your code will pass the CI checks.
When the review process is complete, you will be asked to **squash** the commits and **rebase** your branch.
**Do not merge** the `main` branch into your branch, instead, rebase your branch on top of the latest `main`
branch after **syncing your fork** with the latest changes from the Flux repository. After rebasing,
you can push your branch with the `--force-with-lease` option to update the PR.
## Communications
For realtime communications we use Slack. To reach out to the Flux maintainers and contributors,
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the [#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
To discuss ideas and specifications we use [GitHub Discussions](https://github.com/fluxcd/flux2/discussions).
For announcements, we use a mailing list as well. Subscribe to
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev), there you can also add calendar invites
to your Google calendar for our [Flux dev meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view).
+3 -3
View File
@@ -1,16 +1,16 @@
FROM alpine:3.23 AS builder FROM alpine:3.22 AS builder
RUN apk add --no-cache ca-certificates curl RUN apk add --no-cache ca-certificates curl
ARG ARCH=linux/amd64 ARG ARCH=linux/amd64
ARG KUBECTL_VER=1.35.0 ARG KUBECTL_VER=1.34.0
RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \ RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl -o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl
RUN kubectl version --client=true RUN kubectl version --client=true
FROM alpine:3.23 AS flux-cli FROM alpine:3.22 AS flux-cli
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates
+2 -2
View File
@@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
all: test build all: test build
tidy: tidy:
go mod tidy -compat=1.26 go mod tidy -compat=1.25
cd tests/integration && go mod tidy -compat=1.26 cd tests/integration && go mod tidy -compat=1.25
fmt: fmt:
go fmt ./... go fmt ./...
+1 -3
View File
@@ -52,14 +52,12 @@ guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/).
### Components ### Components
- [Source Controllers](https://fluxcd.io/flux/components/source/) - [Source Controller](https://fluxcd.io/flux/components/source/)
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/) - [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/) - [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/) - [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/) - [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/) - [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
- [ExternalArtifact CRD](https://fluxcd.io/flux/components/source/externalartifacts/)
- [ArtifactGenerator CRD](https://fluxcd.io/flux/components/source/artifactgenerators/)
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/) - [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/) - [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
- [Helm Controller](https://fluxcd.io/flux/components/helm/) - [Helm Controller](https://fluxcd.io/flux/components/helm/)
+3 -31
View File
@@ -77,37 +77,9 @@ runs:
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/" FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
MAX_RETRIES=5 curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"
RETRY_DELAY=5 curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"
for i in $(seq 1 $MAX_RETRIES); do
echo "Downloading flux binary (attempt $i/$MAX_RETRIES)"
if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then
break
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
sleep $RETRY_DELAY
else
echo "Failed to download flux binary after $MAX_RETRIES attempts"
exit 1
fi
done
for i in $(seq 1 $MAX_RETRIES); do
echo "Downloading checksums file (attempt $i/$MAX_RETRIES)"
if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then
break
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
sleep $RETRY_DELAY
else
echo "Failed to download checksums file after $MAX_RETRIES attempts"
exit 1
fi
done
echo "Verifying checksum" echo "Verifying checksum"
sum="" sum=""
if command -v openssl > /dev/null; then if command -v openssl > /dev/null; then
-90
View File
@@ -20,11 +20,9 @@ import (
"context" "context"
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/signature"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@@ -32,7 +30,6 @@ import (
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/bootstrap"
"github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
@@ -82,11 +79,6 @@ type bootstrapFlags struct {
gpgPassphrase string gpgPassphrase string
gpgKeyID string gpgKeyID string
sshSigningKeyFile string
sshSigningPassword string
sshSigningPassphrase string
sshSigningReusePrivateKey bool
force bool force bool
commitMessageAppendix string commitMessageAppendix string
@@ -147,12 +139,6 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningKeyFile, "ssh-signing-key-file", "", "path to an SSH private key file used for signing commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningPassword, "ssh-signing-password", "", "passphrase for decrypting SSH signing key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningPassphrase, "ssh-signing-passphrase", "", "alias for --ssh-signing-password")
bootstrapCmd.PersistentFlags().MarkHidden("ssh-signing-passphrase")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.sshSigningReusePrivateKey, "ssh-signing-reuse-private-key", false, "use the SSH transport key (--private-key-file) to sign commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
@@ -209,31 +195,6 @@ func bootstrapValidate() error {
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
} }
sshSigningSet := bootstrapArgs.sshSigningKeyFile != "" || bootstrapArgs.sshSigningReusePrivateKey
if bootstrapArgs.gpgKeyRingPath != "" && sshSigningSet {
return fmt.Errorf("--gpg-* and --ssh-signing-* are mutually exclusive; pick one signing format")
}
if bootstrapArgs.sshSigningKeyFile != "" && bootstrapArgs.sshSigningReusePrivateKey {
return fmt.Errorf("--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive")
}
if bootstrapArgs.sshSigningReusePrivateKey && bootstrapArgs.privateKeyFile == "" {
return fmt.Errorf("--ssh-signing-reuse-private-key requires --private-key-file")
}
sshSigningPwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
if sshSigningPwd != "" && bootstrapArgs.sshSigningKeyFile == "" {
return fmt.Errorf("--ssh-signing-password requires --ssh-signing-key-file")
}
if err := preflightSigningKey(); err != nil {
return err
}
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 { if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 {
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
} }
@@ -253,57 +214,6 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string {
return m return m
} }
// preflightSigningKey reads and parses the configured signing key so
// malformed PEM, wrong passphrases, and unsupported SSH algorithms
// surface before any clone runs.
func preflightSigningKey() error {
switch {
case bootstrapArgs.gpgKeyRingPath != "":
ring, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if err != nil {
return fmt.Errorf("invalid GPG signing key: %w", err)
}
if _, err := bootstrap.SelectOpenPGPSigningEntity(ring, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID); err != nil {
return fmt.Errorf("invalid GPG signing key: %w", err)
}
case bootstrapArgs.sshSigningKeyFile != "":
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH signing key file: %w", err)
}
pwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
if _, err := signature.NewSSHSigner(pemBytes, []byte(pwd)); err != nil {
return fmt.Errorf("invalid SSH signing key: %w", err)
}
}
return nil
}
// effectiveSshSigningPassword resolves the SSH signing-key passphrase
// from --ssh-signing-password and its hidden alias
// --ssh-signing-passphrase. When both are set with the same value, the
// value is returned. When both are set with different non-empty values,
// an error is returned. When neither is set, an empty string is
// returned with no error.
func effectiveSshSigningPassword() (string, error) {
pw := bootstrapArgs.sshSigningPassword
alias := bootstrapArgs.sshSigningPassphrase
switch {
case pw != "" && alias != "":
if pw != alias {
return "", fmt.Errorf("--ssh-signing-password and --ssh-signing-passphrase are aliases; do not pass both")
}
return pw, nil
case pw == "" && alias != "":
return alias, nil
default:
return pw, nil
}
}
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation. // confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation.
// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation // It returns a nil error if Flux is not installed or the user confirms overriding an existing installation
func confirmBootstrap(ctx context.Context, kubeClient client.Client) error { func confirmBootstrap(ctx context.Context, kubeClient client.Client) error {
-26
View File
@@ -24,7 +24,6 @@ import (
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit" "github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/pkg/git/signature"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
@@ -288,31 +287,6 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
} }
if bootstrapArgs.sshSigningKeyFile != "" {
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH signing key file: %w", err)
}
pwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
}
if bootstrapArgs.sshSigningReusePrivateKey {
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
if err != nil {
return fmt.Errorf("failed to read transport private key for signing: %w", err)
}
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
}
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
if err != nil { if err != nil {
-28
View File
@@ -30,7 +30,6 @@ import (
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit" "github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/pkg/git/signature"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
@@ -316,33 +315,6 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshSigningKeyFile != "" {
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH signing key file: %w", err)
}
pwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
}
if bootstrapArgs.sshSigningReusePrivateKey {
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
if err != nil {
return fmt.Errorf("failed to read transport private key for signing: %w", err)
}
// Reuse-path pre-flight: bootstrapValidate cannot run this check
// because the SSH transport password is subcommand-local.
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
}
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...) b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
if err != nil { if err != nil {
-19
View File
@@ -252,12 +252,6 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshSigningReusePrivateKey {
return fmt.Errorf("--ssh-signing-reuse-private-key is not supported by 'bootstrap gitea'; " +
"that subcommand generates the SSH transport key in-process and has no operator-supplied key to reuse")
}
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
} }
@@ -271,19 +265,6 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
} }
if bootstrapArgs.sshSigningKeyFile != "" {
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH signing key file: %w", err)
}
pwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
}
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
if err != nil { if err != nil {
-19
View File
@@ -259,12 +259,6 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshSigningReusePrivateKey {
return fmt.Errorf("--ssh-signing-reuse-private-key is not supported by 'bootstrap github'; " +
"that subcommand generates the SSH transport key in-process and has no operator-supplied key to reuse")
}
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
} }
@@ -278,19 +272,6 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
} }
if bootstrapArgs.sshSigningKeyFile != "" {
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH signing key file: %w", err)
}
pwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
}
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
if err != nil { if err != nil {
-26
View File
@@ -27,7 +27,6 @@ import (
"github.com/fluxcd/go-git-providers/gitprovider" "github.com/fluxcd/go-git-providers/gitprovider"
"github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit" "github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/pkg/git/signature"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
@@ -322,31 +321,6 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
} }
if bootstrapArgs.sshSigningKeyFile != "" {
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
if err != nil {
return fmt.Errorf("failed to read SSH signing key file: %w", err)
}
pwd, err := effectiveSshSigningPassword()
if err != nil {
return err
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
}
if bootstrapArgs.sshSigningReusePrivateKey {
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
if err != nil {
return fmt.Errorf("failed to read transport private key for signing: %w", err)
}
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
}
bootstrapOpts = append(bootstrapOpts,
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
}
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
if err != nil { if err != nil {
-155
View File
@@ -1,155 +0,0 @@
/*
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 (
"strings"
"testing"
)
func TestBootstrapValidate_signingFlags(t *testing.T) {
tests := []struct {
name string
gpgRing string
gpgPass string
sshKey string
sshPass string
sshPassp string
privateKey string
reuse bool
wantErr string
}{
{name: "no signing flags is valid"},
{name: "GPG only is valid", gpgRing: "./testdata/bootstrap/gpg.pgp"},
{name: "SSH only is valid", sshKey: "./testdata/bootstrap/ed25519.private"},
{
name: "Reuse-private-key with private-key-file is valid",
privateKey: "./testdata/bootstrap/ed25519.private",
reuse: true,
},
{
name: "GPG + SSH errors",
gpgRing: "./testdata/bootstrap/gpg.pgp",
sshKey: "./testdata/bootstrap/ed25519.private",
wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive",
},
{
name: "GPG + reuse errors",
gpgRing: "./testdata/bootstrap/gpg.pgp",
privateKey: "./testdata/bootstrap/ed25519.private",
reuse: true,
wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive",
},
{
name: "SSH key-file + reuse errors",
sshKey: "./testdata/bootstrap/ed25519.private",
privateKey: "./testdata/bootstrap/ed25519.private",
reuse: true,
wantErr: "--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive",
},
{
name: "Reuse without private-key-file errors",
reuse: true,
wantErr: "--ssh-signing-reuse-private-key requires --private-key-file",
},
{
name: "SSH password without key errors",
sshPass: "secret",
wantErr: "--ssh-signing-password requires --ssh-signing-key-file",
},
{
name: "SSH passphrase alias alone applies",
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
sshPassp: "abcde12345",
},
{
name: "SSH password and passphrase with same value passes",
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
sshPass: "abcde12345",
sshPassp: "abcde12345",
},
{
name: "SSH password and passphrase with different values errors",
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
sshPass: "right",
sshPassp: "wrong",
wantErr: "are aliases; do not pass both",
},
{
name: "SSH malformed key fails pre-flight",
sshKey: "./testdata/bootstrap/malformed.private",
wantErr: "invalid SSH signing key",
},
{
name: "SSH encrypted key without password fails pre-flight",
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
wantErr: "passphrase required",
},
// The GPG fixture used here is encrypted (passphrase: "right") so that
// passing the wrong passphrase exercises the Decrypt error path.
// An unencrypted key would make Decrypt a no-op regardless of the
// passphrase supplied.
{
name: "GPG with wrong passphrase fails pre-flight",
gpgRing: "./testdata/bootstrap/gpg-encrypted.pgp",
gpgPass: "wrong",
wantErr: "invalid GPG signing key",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
savedGpgRing := bootstrapArgs.gpgKeyRingPath
savedGpgPass := bootstrapArgs.gpgPassphrase
savedSshKey := bootstrapArgs.sshSigningKeyFile
savedSshPass := bootstrapArgs.sshSigningPassword
savedSshPassp := bootstrapArgs.sshSigningPassphrase
savedPrivKey := bootstrapArgs.privateKeyFile
savedReuse := bootstrapArgs.sshSigningReusePrivateKey
defer func() {
bootstrapArgs.gpgKeyRingPath = savedGpgRing
bootstrapArgs.gpgPassphrase = savedGpgPass
bootstrapArgs.sshSigningKeyFile = savedSshKey
bootstrapArgs.sshSigningPassword = savedSshPass
bootstrapArgs.sshSigningPassphrase = savedSshPassp
bootstrapArgs.privateKeyFile = savedPrivKey
bootstrapArgs.sshSigningReusePrivateKey = savedReuse
}()
bootstrapArgs.gpgKeyRingPath = tt.gpgRing
bootstrapArgs.gpgPassphrase = tt.gpgPass
bootstrapArgs.sshSigningKeyFile = tt.sshKey
bootstrapArgs.sshSigningPassword = tt.sshPass
bootstrapArgs.sshSigningPassphrase = tt.sshPassp
bootstrapArgs.privateKeyFile = tt.privateKey
bootstrapArgs.sshSigningReusePrivateKey = tt.reuse
err := bootstrapValidate()
if tt.wantErr == "" {
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
}
})
}
}
+3 -150
View File
@@ -22,7 +22,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -49,10 +48,9 @@ from the given directory or a single manifest file.`,
} }
type buildArtifactFlags struct { type buildArtifactFlags struct {
output string output string
path string path string
ignorePaths []string ignorePaths []string
resolveSymlinks bool
} }
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...) var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
@@ -63,7 +61,6 @@ func init() {
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.") buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.") buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
buildArtifactCmd.Flags().BoolVar(&buildArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
buildCmd.AddCommand(buildArtifactCmd) buildCmd.AddCommand(buildArtifactCmd)
} }
@@ -88,15 +85,6 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path) return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
} }
if buildArtifactArgs.resolveSymlinks {
resolved, cleanupDir, err := resolveSymlinks(path)
if err != nil {
return fmt.Errorf("resolving symlinks failed: %w", err)
}
defer os.RemoveAll(cleanupDir)
path = resolved
}
logger.Actionf("building artifact from %s", path) logger.Actionf("building artifact from %s", path)
ociClient := oci.NewClient(oci.DefaultOptions()) ociClient := oci.NewClient(oci.DefaultOptions())
@@ -108,141 +96,6 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
return nil return nil
} }
// resolveSymlinks creates a temporary directory with symlinks resolved to their
// real file contents. This allows building artifacts from symlink trees (e.g.,
// those created by Nix) where the actual files live outside the source directory.
// It returns the resolved path and the temporary directory path for cleanup.
func resolveSymlinks(srcPath string) (string, string, error) {
absPath, err := filepath.Abs(srcPath)
if err != nil {
return "", "", err
}
info, err := os.Stat(absPath)
if err != nil {
return "", "", err
}
// For a single file, resolve the symlink and return the path to the
// copied file within the temp dir, preserving file semantics for callers.
if !info.IsDir() {
resolved, err := filepath.EvalSymlinks(absPath)
if err != nil {
return "", "", fmt.Errorf("resolving symlink for %s: %w", absPath, err)
}
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
if err != nil {
return "", "", err
}
dst := filepath.Join(tmpDir, filepath.Base(absPath))
if err := copyFile(resolved, dst); err != nil {
os.RemoveAll(tmpDir)
return "", "", err
}
return dst, tmpDir, nil
}
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
if err != nil {
return "", "", err
}
visited := make(map[string]bool)
if err := copyDir(absPath, tmpDir, visited); err != nil {
os.RemoveAll(tmpDir)
return "", "", err
}
return tmpDir, tmpDir, nil
}
// copyDir recursively copies the contents of srcDir to dstDir, resolving any
// symlinks encountered along the way. The visited map tracks resolved real
// directory paths to detect and break symlink cycles.
func copyDir(srcDir, dstDir string, visited map[string]bool) error {
real, err := filepath.EvalSymlinks(srcDir)
if err != nil {
return fmt.Errorf("resolving symlink %s: %w", srcDir, err)
}
abs, err := filepath.Abs(real)
if err != nil {
return fmt.Errorf("getting absolute path for %s: %w", real, err)
}
if visited[abs] {
return nil // break the cycle
}
visited[abs] = true
defer delete(visited, abs)
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(srcDir, entry.Name())
dstPath := filepath.Join(dstDir, entry.Name())
// Resolve symlinks to get the real path and info.
realPath, err := filepath.EvalSymlinks(srcPath)
if err != nil {
return fmt.Errorf("resolving symlink %s: %w", srcPath, err)
}
realInfo, err := os.Stat(realPath)
if err != nil {
return fmt.Errorf("stat resolved path %s: %w", realPath, err)
}
if realInfo.IsDir() {
if err := os.MkdirAll(dstPath, realInfo.Mode()); err != nil {
return err
}
// Recursively copy the resolved directory contents.
if err := copyDir(realPath, dstPath, visited); err != nil {
return err
}
continue
}
if !realInfo.Mode().IsRegular() {
continue
}
if err := copyFile(realPath, dstPath); err != nil {
return err
}
}
return nil
}
func copyFile(src, dst string) error {
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Close()
}
func saveReaderToFile(reader io.Reader) (string, error) { func saveReaderToFile(reader io.Reader) (string, error) {
b, err := io.ReadAll(bufio.NewReader(reader)) b, err := io.ReadAll(bufio.NewReader(reader))
if err != nil { if err != nil {
-147
View File
@@ -18,7 +18,6 @@ package main
import ( import (
"os" "os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@@ -69,149 +68,3 @@ data:
} }
} }
func Test_resolveSymlinks(t *testing.T) {
g := NewWithT(t)
// Create source directory with a real file
srcDir := t.TempDir()
realFile := filepath.Join(srcDir, "real.yaml")
g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n name: test\n"), 0o644)).To(Succeed())
// Create a directory with symlinks pointing to files outside it
symlinkDir := t.TempDir()
symlinkFile := filepath.Join(symlinkDir, "linked.yaml")
g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed())
// Also add a regular file in the symlink dir
regularFile := filepath.Join(symlinkDir, "regular.yaml")
g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed())
// Create a symlinked subdirectory
subDir := filepath.Join(srcDir, "subdir")
g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed())
g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed())
g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed())
// Resolve symlinks
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
g.Expect(err).To(BeNil())
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
// Verify the regular file was copied
content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n"))
// Verify the symlinked file was resolved and copied
content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(ContainSubstring("kind: Namespace"))
// Verify that the resolved file is a regular file, not a symlink
info, err := os.Lstat(filepath.Join(resolved, "linked.yaml"))
g.Expect(err).To(BeNil())
g.Expect(info.Mode().IsRegular()).To(BeTrue())
// Verify that the symlinked directory was resolved and its contents were copied
content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(Equal("nested"))
// Verify that the file inside the symlinked directory is a regular file
info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml"))
g.Expect(err).To(BeNil())
g.Expect(info.Mode().IsRegular()).To(BeTrue())
}
func Test_resolveSymlinks_singleFile(t *testing.T) {
g := NewWithT(t)
// Create a real file
srcDir := t.TempDir()
realFile := filepath.Join(srcDir, "manifest.yaml")
g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed())
// Create a symlink to the real file
linkDir := t.TempDir()
linkFile := filepath.Join(linkDir, "link.yaml")
g.Expect(os.Symlink(realFile, linkFile)).To(Succeed())
// Resolve the single symlinked file
resolved, cleanupDir, err := resolveSymlinks(linkFile)
g.Expect(err).To(BeNil())
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
// The returned path should be a file, not a directory
info, err := os.Stat(resolved)
g.Expect(err).To(BeNil())
g.Expect(info.IsDir()).To(BeFalse())
// Verify contents
content, err := os.ReadFile(resolved)
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(Equal("kind: ConfigMap"))
}
func Test_resolveSymlinks_cycle(t *testing.T) {
g := NewWithT(t)
// Create a directory with a symlink cycle: dir/link -> dir
dir := t.TempDir()
g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed())
// resolveSymlinks should not infinite-loop
resolved, cleanupDir, err := resolveSymlinks(dir)
g.Expect(err).To(BeNil())
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
// The file should be copied
content, err := os.ReadFile(filepath.Join(resolved, "file.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(Equal("data"))
// The cycle directory should exist but not cause infinite nesting
_, err = os.Stat(filepath.Join(resolved, "cycle"))
g.Expect(err).To(BeNil())
// There should NOT be deeply nested cycle/cycle/cycle/... paths
_, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle"))
g.Expect(os.IsNotExist(err)).To(BeTrue())
}
func Test_resolveSymlinks_multipleLinksSameTarget(t *testing.T) {
g := NewWithT(t)
// Create source directory with a real file inside a dir
srcDir := t.TempDir()
targetDir := filepath.Join(srcDir, "target")
g.Expect(os.MkdirAll(targetDir, 0o755)).To(Succeed())
g.Expect(os.WriteFile(filepath.Join(targetDir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
// Create a directory with multiple symlinks pointing to targetDir
symlinkDir := t.TempDir()
// Link 1
link1 := filepath.Join(symlinkDir, "link1")
g.Expect(os.Symlink(targetDir, link1)).To(Succeed())
// Link 2
link2 := filepath.Join(symlinkDir, "link2")
g.Expect(os.Symlink(targetDir, link2)).To(Succeed())
// Resolve symlinks
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
g.Expect(err).To(BeNil())
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
// Verify link1 has the file
content, err := os.ReadFile(filepath.Join(resolved, "link1", "file.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content)).To(Equal("data"))
// Verify link2 ALSO has the file
content2, err := os.ReadFile(filepath.Join(resolved, "link2", "file.yaml"))
g.Expect(err).To(BeNil())
g.Expect(string(content2)).To(Equal("data"))
}
-13
View File
@@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -72,7 +71,6 @@ type buildKsFlags struct {
strictSubst bool strictSubst bool
recursive bool recursive bool
localSources map[string]string localSources map[string]string
inMemoryBuild bool
} }
var buildKsArgs buildKsFlags var buildKsArgs buildKsFlags
@@ -86,8 +84,6 @@ func init() {
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.") "When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations") buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path") buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
buildKsCmd.Flags().BoolVar(&buildKsArgs.inMemoryBuild, "in-memory-build", true,
"Use in-memory filesystem during build.")
buildCmd.AddCommand(buildKsCmd) buildCmd.AddCommand(buildKsCmd)
} }
@@ -101,13 +97,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
return fmt.Errorf("invalid resource path %q", buildKsArgs.path) return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
} }
// Normalize the path to handle Windows absolute and relative paths correctly
buildKsArgs.path, err = filepath.Abs(buildKsArgs.path)
if err != nil {
return fmt.Errorf("failed to resolve absolute path: %w", err)
}
buildKsArgs.path = filepath.Clean(buildKsArgs.path)
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() { if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
return fmt.Errorf("invalid resource path %q", buildKsArgs.path) return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
} }
@@ -133,7 +122,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithStrictSubstitute(buildKsArgs.strictSubst),
build.WithRecursive(buildKsArgs.recursive), build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources), build.WithLocalSources(buildKsArgs.localSources),
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
) )
} else { } else {
builder, err = build.NewBuilder(name, buildKsArgs.path, builder, err = build.NewBuilder(name, buildKsArgs.path,
@@ -144,7 +132,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithStrictSubstitute(buildKsArgs.strictSubst),
build.WithRecursive(buildKsArgs.recursive), build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources), build.WithLocalSources(buildKsArgs.localSources),
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
) )
} }
-104
View File
@@ -52,12 +52,6 @@ func TestBuildKustomization(t *testing.T) {
resultFile: "./testdata/build-kustomization/podinfo-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build podinfo (on-disk)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{ {
name: "build podinfo without service", name: "build podinfo without service",
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
@@ -76,24 +70,12 @@ func TestBuildKustomization(t *testing.T) {
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build ignore (on-disk)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=false",
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{ {
name: "build with recursive", name: "build with recursive",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization", args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build with recursive (on-disk)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{
@@ -163,12 +145,6 @@ spec:
resultFile: "./testdata/build-kustomization/podinfo-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build podinfo (on-disk)",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{ {
name: "build podinfo without service", name: "build podinfo without service",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
@@ -199,18 +175,6 @@ spec:
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build with recursive (on-disk)",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive in dry-run mode (on-disk)",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{
@@ -254,71 +218,3 @@ spec:
}) })
} }
} }
// TestBuildKustomizationPathNormalization verifies that absolute and complex
// paths are normalized to prevent path concatenation bugs (issue #5673).
// Without normalization, paths could be duplicated like: /path/test/path/test/file
func TestBuildKustomizationPathNormalization(t *testing.T) {
// Get absolute path to testdata to test absolute path handling
absTestDataPath, err := filepath.Abs("testdata/build-kustomization/podinfo")
if err != nil {
t.Fatalf("failed to get absolute path: %v", err)
}
tests := []struct {
name string
args string
resultFile string
assertFunc string
}{
{
name: "build with absolute path",
args: "build kustomization podinfo --path " + absTestDataPath,
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with absolute path (on-disk)",
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=false",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with complex relative path (parent dir)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with path containing redundant separators",
args: "build kustomization podinfo --path ./testdata//build-kustomization//podinfo",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setup(t, tmpl)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var assert assertFunc
switch tt.assertFunc {
case "assertGoldenTemplateFile":
assert = assertGoldenTemplateFile(tt.resultFile, tmpl)
case "assertError":
assert = assertError(tt.resultFile)
}
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: assert,
}
cmd.runTestCmd(t)
})
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ type checkFlags struct {
} }
var kubernetesConstraints = []string{ var kubernetesConstraints = []string{
">=1.33.0-0", ">=1.32.0-0",
} }
var checkArgs checkFlags var checkArgs checkFlags
-4
View File
@@ -182,10 +182,6 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("chart or chart-ref is required") return fmt.Errorf("chart or chart-ref is required")
} }
if helmReleaseArgs.chart != "" && helmReleaseArgs.chartRef != "" {
return fmt.Errorf("cannot use --chart in combination with --chart-ref")
}
sourceLabels, err := parseLabels() sourceLabels, err := parseLabels()
if err != nil { if err != nil {
return err return err
-5
View File
@@ -42,11 +42,6 @@ func TestCreateHelmRelease(t *testing.T) {
args: "create helmrelease podinfo --export", args: "create helmrelease podinfo --export",
assert: assertError("chart or chart-ref is required"), assert: assertError("chart or chart-ref is required"),
}, },
{
name: "chart and chartRef used in combination",
args: "create helmrelease podinfo --chart podinfo --chart-ref foobar/podinfo --export",
assert: assertError("cannot use --chart in combination with --chart-ref"),
},
{ {
name: "unknown source kind", name: "unknown source kind",
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export", args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
-25
View File
@@ -23,7 +23,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1" autov1 "github.com/fluxcd/image-automation-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
) )
@@ -76,8 +75,6 @@ type imageUpdateFlags struct {
commitTemplate string commitTemplate string
authorName string authorName string
authorEmail string authorEmail string
signingKeySecret string
signingKeyType string
} }
var imageUpdateArgs = imageUpdateFlags{} var imageUpdateArgs = imageUpdateFlags{}
@@ -92,8 +89,6 @@ func init() {
flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages") flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages")
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author") flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author") flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
flags.StringVar(&imageUpdateArgs.signingKeySecret, "signing-key-secret", "", "name of the Secret containing the signing key referenced in spec.git.commit.signingKey")
flags.StringVar(&imageUpdateArgs.signingKeyType, "signing-key-type", "", "signing-key format: gpg or ssh (defaults to gpg when --signing-key-secret is set)")
createImageCmd.AddCommand(createImageUpdateCmd) createImageCmd.AddCommand(createImageUpdateCmd)
} }
@@ -117,15 +112,6 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("the author email is required (--author-email)") return fmt.Errorf("the author email is required (--author-email)")
} }
if imageUpdateArgs.signingKeyType != "" && imageUpdateArgs.signingKeySecret == "" {
return fmt.Errorf("--signing-key-type requires --signing-key-secret")
}
if imageUpdateArgs.signingKeyType != "" &&
imageUpdateArgs.signingKeyType != string(autov1.SigningKeyTypeGPG) &&
imageUpdateArgs.signingKeyType != string(autov1.SigningKeyTypeSSH) {
return fmt.Errorf("--signing-key-type must be one of: gpg, ssh")
}
labels, err := parseLabels() labels, err := parseLabels()
if err != nil { if err != nil {
return err return err
@@ -177,17 +163,6 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
} }
} }
if imageUpdateArgs.signingKeySecret != "" {
keyType := imageUpdateArgs.signingKeyType
if keyType == "" {
keyType = string(autov1.SigningKeyTypeGPG)
}
update.Spec.GitSpec.Commit.SigningKey = &autov1.SigningKey{
SecretRef: meta.LocalObjectReference{Name: imageUpdateArgs.signingKeySecret},
Type: autov1.SigningKeyType(keyType),
}
}
if createArgs.export { if createArgs.export {
return printExport(exportImageUpdate(&update)) return printExport(exportImageUpdate(&update))
} }
-62
View File
@@ -1,62 +0,0 @@
/*
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 "testing"
func TestCreateImageUpdate(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "no signing key",
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --interval=1m0s --namespace=flux-system --export",
assert: assertGoldenFile("./testdata/create_image_update/no-signing.yaml"),
},
{
name: "signing secret without explicit type defaults to gpg",
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=my-key --interval=1m0s --namespace=flux-system --export",
assert: assertGoldenFile("./testdata/create_image_update/signing-default-gpg.yaml"),
},
{
name: "ssh signing key",
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=my-deploy-key --signing-key-type=ssh --interval=1m0s --namespace=flux-system --export",
assert: assertGoldenFile("./testdata/create_image_update/signing-ssh.yaml"),
},
{
name: "signing-key-type without secret errors",
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-type=ssh --namespace=flux-system --export",
assert: assertError("--signing-key-type requires --signing-key-secret"),
},
{
name: "invalid signing-key-type errors",
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=k --signing-key-type=pgp --namespace=flux-system --export",
assert: assertError("--signing-key-type must be one of: gpg, ssh"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
-3
View File
@@ -136,9 +136,6 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") { if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
return fmt.Errorf("path must begin with ./") return fmt.Errorf("path must begin with ./")
} }
if kustomizationArgs.source.Name == "" {
return fmt.Errorf("source is required")
}
if !createArgs.export { if !createArgs.export {
logger.Generatef("generating Kustomization") logger.Generatef("generating Kustomization")
-48
View File
@@ -1,48 +0,0 @@
//go:build unit
// +build unit
/*
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 "testing"
func TestCreateKustomization(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
// A user creating a kustomization without --source gets a confusing
// API-level error about spec.sourceRef.kind instead of a clear message.
name: "missing source",
args: "create kustomization my-app --path=./deploy --export",
assert: assertError("source is required"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
+3 -4
View File
@@ -30,7 +30,6 @@ import (
notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
@@ -50,7 +49,7 @@ var createReceiverCmd = &cobra.Command{
} }
type receiverFlags struct { type receiverFlags struct {
receiverType flags.ReceiverType receiverType string
secretRef string secretRef string
events []string events []string
resources []string resources []string
@@ -59,7 +58,7 @@ type receiverFlags struct {
var receiverArgs receiverFlags var receiverArgs receiverFlags
func init() { func init() {
createReceiverCmd.Flags().Var(&receiverArgs.receiverType, "type", receiverArgs.receiverType.Description()) createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "")
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "") createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values") createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values") createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
@@ -110,7 +109,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: notificationv1.ReceiverSpec{ Spec: notificationv1.ReceiverSpec{
Type: receiverArgs.receiverType.String(), Type: receiverArgs.receiverType,
Events: receiverArgs.events, Events: receiverArgs.events,
Resources: resources, Resources: resources,
SecretRef: meta.LocalObjectReference{ SecretRef: meta.LocalObjectReference{
-16
View File
@@ -56,22 +56,6 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
} }
existing.StringData = secret.StringData existing.StringData = secret.StringData
if secret.Annotations != nil {
if existing.Annotations == nil {
existing.Annotations = make(map[string]string)
}
for k, v := range secret.Annotations {
existing.Annotations[k] = v
}
}
if secret.Labels != nil {
if existing.Labels == nil {
existing.Labels = make(map[string]string)
}
for k, v := range secret.Labels {
existing.Labels[k] = v
}
}
if err := kubeClient.Update(ctx, &existing); err != nil { if err := kubeClient.Update(ctx, &existing); err != nil {
return err return err
} }
+25 -13
View File
@@ -46,18 +46,16 @@ var createSecretGitHubAppCmd = &cobra.Command{
} }
type secretGitHubAppFlags struct { type secretGitHubAppFlags struct {
appID string appID string
appInstallationOwner string appInstallationID string
appInstallationID string privateKeyFile string
privateKeyFile string baseURL string
baseURL string
} }
var secretGitHubAppArgs = secretGitHubAppFlags{} var secretGitHubAppArgs = secretGitHubAppFlags{}
func init() { func init() {
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationOwner, "app-installation-owner", "", "github app installation owner (user or organization)")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
@@ -72,19 +70,33 @@ func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
secretName := args[0] secretName := args[0]
if secretGitHubAppArgs.appID == "" {
return fmt.Errorf("--app-id is required")
}
if secretGitHubAppArgs.appInstallationID == "" {
return fmt.Errorf("--app-installation-id is required")
}
if secretGitHubAppArgs.privateKeyFile == "" {
return fmt.Errorf("--app-private-key is required")
}
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile) privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
if err != nil { if err != nil {
return fmt.Errorf("unable to read private key file: %w", err) return fmt.Errorf("unable to read private key file: %w", err)
} }
opts := sourcesecret.Options{ opts := sourcesecret.Options{
Name: secretName, Name: secretName,
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
GitHubAppID: secretGitHubAppArgs.appID, GitHubAppID: secretGitHubAppArgs.appID,
GitHubAppInstallationOwner: secretGitHubAppArgs.appInstallationOwner, GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID, GitHubAppPrivateKey: string(privateKey),
GitHubAppPrivateKey: string(privateKey), }
GitHubAppBaseURL: secretGitHubAppArgs.baseURL,
if secretGitHubAppArgs.baseURL != "" {
opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL
} }
secret, err := sourcesecret.GenerateGitHubApp(opts) secret, err := sourcesecret.GenerateGitHubApp(opts)
+16 -1
View File
@@ -31,6 +31,21 @@ func TestCreateSecretGitHubApp(t *testing.T) {
args: "create secret githubapp", args: "create secret githubapp",
assert: assertError("name is required"), assert: assertError("name is required"),
}, },
{
name: "create githubapp secret with missing app-id",
args: "create secret githubapp appinfo",
assert: assertError("--app-id is required"),
},
{
name: "create githubapp secret with missing appInstallationID",
args: "create secret githubapp appinfo --app-id 1",
assert: assertError("--app-installation-id is required"),
},
{
name: "create githubapp secret with missing private key file",
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2",
assert: assertError("--app-private-key is required"),
},
{ {
name: "create githubapp secret with private key file that does not exist", name: "create githubapp secret with private key file that does not exist",
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem", args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
@@ -38,7 +53,7 @@ func TestCreateSecretGitHubApp(t *testing.T) {
}, },
{ {
name: "create githubapp secret with app info", name: "create githubapp secret with app info",
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-owner my-org --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export", args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"), assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
}, },
{ {
-134
View File
@@ -1,134 +0,0 @@
/*
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 (
"context"
"fmt"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
)
var createSecretReceiverCmd = &cobra.Command{
Use: "receiver [name]",
Short: "Create or update a Kubernetes secret for a Receiver webhook",
Long: `The create secret receiver command generates a Kubernetes secret with
the token used for webhook payload validation and an annotation with the
computed webhook URL.`,
Example: ` # Create a receiver secret for a GitHub webhook
flux create secret receiver github-receiver \
--namespace=my-namespace \
--type=github \
--hostname=flux.example.com \
--export
# Create a receiver secret for GCR with email claim
flux create secret receiver gcr-receiver \
--namespace=my-namespace \
--type=gcr \
--hostname=flux.example.com \
--email-claim=sa@project.iam.gserviceaccount.com \
--export`,
RunE: createSecretReceiverCmdRun,
}
type secretReceiverFlags struct {
receiverType flags.ReceiverType
token string
hostname string
emailClaim string
audienceClaim string
}
var secretReceiverArgs secretReceiverFlags
func init() {
createSecretReceiverCmd.Flags().Var(&secretReceiverArgs.receiverType, "type", secretReceiverArgs.receiverType.Description())
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.token, "token", "", "webhook token used for payload validation and URL computation, auto-generated if not specified")
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.hostname, "hostname", "", "hostname for the webhook URL e.g. flux.example.com")
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.emailClaim, "email-claim", "", "IAM service account email, required for gcr type")
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.audienceClaim, "audience-claim", "", "custom OIDC token audience for gcr type, defaults to the webhook URL")
createSecretCmd.AddCommand(createSecretReceiverCmd)
}
func createSecretReceiverCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if secretReceiverArgs.receiverType == "" {
return fmt.Errorf("--type is required")
}
if secretReceiverArgs.hostname == "" {
return fmt.Errorf("--hostname is required")
}
if secretReceiverArgs.receiverType.String() == notificationv1.GCRReceiver && secretReceiverArgs.emailClaim == "" {
return fmt.Errorf("--email-claim is required for gcr receiver type")
}
labels, err := parseLabels()
if err != nil {
return err
}
opts := sourcesecret.Options{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
ReceiverType: secretReceiverArgs.receiverType.String(),
Token: secretReceiverArgs.token,
Hostname: secretReceiverArgs.hostname,
EmailClaim: secretReceiverArgs.emailClaim,
AudienceClaim: secretReceiverArgs.audienceClaim,
}
secret, err := sourcesecret.GenerateReceiver(opts)
if err != nil {
return err
}
if createArgs.export {
rootCmd.Println(secret.Content)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
}
logger.Actionf("receiver secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil
}
-74
View File
@@ -1,74 +0,0 @@
/*
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 (
"testing"
)
func TestCreateReceiverSecret(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing type",
args: "create secret receiver test-secret --token=t --hostname=h",
assert: assertError("--type is required"),
},
{
name: "invalid type",
args: "create secret receiver test-secret --type=invalid --token=t --hostname=h",
assert: assertError("invalid argument \"invalid\" for \"--type\" flag: receiver type 'invalid' is not supported, must be one of: generic, generic-hmac, github, gitlab, bitbucket, harbor, dockerhub, quay, gcr, nexus, acr, cdevents"),
},
{
name: "missing hostname",
args: "create secret receiver test-secret --type=github --token=t",
assert: assertError("--hostname is required"),
},
{
name: "gcr missing email-claim",
args: "create secret receiver test-secret --type=gcr --token=t --hostname=h",
assert: assertError("--email-claim is required for gcr receiver type"),
},
{
name: "github receiver secret",
args: "create secret receiver receiver-secret --type=github --token=test-token --hostname=flux.example.com --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver.yaml"),
},
{
name: "gcr receiver secret",
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr.yaml"),
},
{
name: "gcr receiver secret with custom audience",
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --audience-claim=https://custom.audience.example.com --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
+6 -9
View File
@@ -114,16 +114,9 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
helmURL, err := url.Parse(sourceHelmArgs.url) if _, err := url.Parse(sourceHelmArgs.url); err != nil {
if err != nil {
return fmt.Errorf("url parse failed: %w", err) return fmt.Errorf("url parse failed: %w", err)
} }
if helmURL.Scheme != "http" && helmURL.Scheme != "https" && helmURL.Scheme != sourcev1.HelmRepositoryTypeOCI {
return fmt.Errorf("url scheme '%s' not supported, can be: http, https and oci", helmURL.Scheme)
}
if helmURL.Host == "" {
return fmt.Errorf("url host is required")
}
helmRepository := &sourcev1.HelmRepository{ helmRepository := &sourcev1.HelmRepository{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@@ -139,7 +132,11 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
}, },
} }
if helmURL.Scheme == sourcev1.HelmRepositoryTypeOCI { url, err := url.Parse(sourceHelmArgs.url)
if err != nil {
return fmt.Errorf("failed to parse URL: %w", err)
}
if url.Scheme == sourcev1.HelmRepositoryTypeOCI {
helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI
helmRepository.Spec.Provider = sourceHelmArgs.ociProvider helmRepository.Spec.Provider = sourceHelmArgs.ociProvider
} }
-6
View File
@@ -36,12 +36,6 @@ func TestCreateSourceHelm(t *testing.T) {
resultFile: "name is required", resultFile: "name is required",
assertFunc: "assertError", assertFunc: "assertError",
}, },
{
name: "unsupported URL scheme",
args: "create source helm podinfo --url=git://example.com/charts --export",
resultFile: "url scheme 'git' not supported, can be: http, https and oci",
assertFunc: "assertError",
},
{ {
name: "OCI repo", name: "OCI repo",
args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export", args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export",
+19 -28
View File
@@ -58,10 +58,9 @@ const (
) )
type tenantFlags struct { type tenantFlags struct {
namespaces []string namespaces []string
clusterRole string clusterRole string
account string account string
skipNamespace bool
} }
var tenantArgs tenantFlags var tenantArgs tenantFlags
@@ -70,7 +69,6 @@ func init() {
createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant") createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant")
createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding") createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding")
createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant") createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant")
createTenantCmd.Flags().BoolVar(&tenantArgs.skipNamespace, "skip-namespace", false, "skip namespace creation (namespace must exist already)")
createCmd.AddCommand(createTenantCmd) createCmd.AddCommand(createTenantCmd)
} }
@@ -159,7 +157,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
if createArgs.export { if createArgs.export {
for i := range tenantArgs.namespaces { for i := range tenantArgs.namespaces {
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i], tenantArgs.skipNamespace); err != nil { if err := exportTenant(namespaces[i], accounts[i], roleBindings[i]); err != nil {
return err return err
} }
} }
@@ -175,11 +173,9 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
} }
for i := range tenantArgs.namespaces { for i := range tenantArgs.namespaces {
if !tenantArgs.skipNamespace { logger.Actionf("applying namespace %s", namespaces[i].Name)
logger.Actionf("applying namespace %s", namespaces[i].Name) if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil { return err
return err
}
} }
logger.Actionf("applying service account %s", accounts[i].Name) logger.Actionf("applying service account %s", accounts[i].Name)
@@ -288,24 +284,19 @@ func upsertRoleBinding(ctx context.Context, kubeClient client.Client, roleBindin
return nil return nil
} }
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding, skipNamespace bool) error { func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding) error {
var data []byte namespace.TypeMeta = metav1.TypeMeta{
var err error APIVersion: "v1",
Kind: "Namespace",
if !skipNamespace {
namespace.TypeMeta = metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
}
data, err = yaml.Marshal(namespace)
if err != nil {
return err
}
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
printlnStdout("---")
printlnStdout(resourceToString(data))
} }
data, err := yaml.Marshal(namespace)
if err != nil {
return err
}
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
printlnStdout("---")
printlnStdout(resourceToString(data))
account.TypeMeta = metav1.TypeMeta{ account.TypeMeta = metav1.TypeMeta{
APIVersion: "v1", APIVersion: "v1",
-5
View File
@@ -54,11 +54,6 @@ func TestCreateTenant(t *testing.T) {
args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export", args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export",
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"), assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"),
}, },
{
name: "tenant with skip namespace",
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --skip-namespace --export",
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-skip-namespace.yaml"),
},
} }
for _, tt := range tests { for _, tt := range tests {
-10
View File
@@ -62,8 +62,6 @@ type diffKsFlags struct {
strictSubst bool strictSubst bool
recursive bool recursive bool
localSources map[string]string localSources map[string]string
inMemoryBuild bool
ignoreNotFound bool
} }
var diffKsArgs diffKsFlags var diffKsArgs diffKsFlags
@@ -77,10 +75,6 @@ func init() {
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.") "When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations") diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path") diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true,
"Use in-memory filesystem during build.")
diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false,
"Ignore Kustomization not found errors on the cluster when diffing.")
diffCmd.AddCommand(diffKsCmd) diffCmd.AddCommand(diffKsCmd)
} }
@@ -119,8 +113,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithRecursive(diffKsArgs.recursive), build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources), build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(), build.WithSingleKustomization(),
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
) )
} else { } else {
builder, err = build.NewBuilder(name, diffKsArgs.path, builder, err = build.NewBuilder(name, diffKsArgs.path,
@@ -132,8 +124,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithRecursive(diffKsArgs.recursive), build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources), build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(), build.WithSingleKustomization(),
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
) )
} }
+2 -114
View File
@@ -48,7 +48,7 @@ func TestDiffKustomization(t *testing.T) {
name: "diff nothing deployed", name: "diff nothing deployed",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
objectFile: "", objectFile: "",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"), assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
}, },
{ {
name: "diff with a deployment object", name: "diff with a deployment object",
@@ -96,7 +96,7 @@ func TestDiffKustomization(t *testing.T) {
name: "diff where kustomization file has multiple objects with the same name", name: "diff where kustomization file has multiple objects with the same name",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
objectFile: "", objectFile: "",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"), assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
}, },
{ {
name: "diff with recursive", name: "diff with recursive",
@@ -138,118 +138,6 @@ func TestDiffKustomization(t *testing.T) {
} }
} }
// TestDiffKustomizationNotDeployed tests `flux diff ks` when the Kustomization
// CR does not exist in the cluster but is provided via --kustomization-file.
// Reproduces https://github.com/fluxcd/flux2/issues/5439
func TestDiffKustomizationNotDeployed(t *testing.T) {
// Use a dedicated namespace with NO setup() -- the Kustomization CR
// intentionally does not exist in the cluster.
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupTestNamespace(tmpl["fluxns"], t)
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "fails without --ignore-not-found",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml",
assert: assertError("failed to get kustomization object: kustomizations.kustomize.toolkit.fluxcd.io \"podinfo\" not found"),
},
{
name: "succeeds with --ignore-not-found and --kustomization-file",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml " +
"--ignore-not-found",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
// TestDiffKustomizationTakeOwnership tests `flux diff ks` when taking ownership
// of existing resources on the cluster. A "pre-existing" configmap is applied
// to the cluster, and the kustomization contains a matching configmap; the
// diff should show the labels added by flux
func TestDiffKustomizationTakeOwnership(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupTestNamespace(tmpl["fluxns"], t)
b, _ := build.NewBuilder("configmaps", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
resourceManager, err := b.Manager()
if err != nil {
t.Fatal(err)
}
// Pre-create the "existing" configmap in the cluster without Flux labels
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/existing-configmap.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
t.Fatal(err)
}
cmd := cmdTestCase{
args: "diff kustomization configmaps --path ./testdata/build-kustomization/configmaps --progress-bar=false " +
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-configmaps.yaml " +
"--ignore-not-found" +
" -n " + tmpl["fluxns"],
assert: assertGoldenFile("./testdata/diff-kustomization/diff-taking-ownership.golden"),
}
cmd.runTestCmd(t)
}
// TestDiffKustomizationNewNamespaceAndConfigmap runs `flux diff ks` when the
// kustomization creates a new namespace and resources inside it. The server-side
// dry-run cannot resolve resources in a namespace that doesn't exist yet,
// consistent with `kubectl diff --server-side` behavior.
func TestDiffKustomizationNewNamespaceAndConfigmap(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupTestNamespace(tmpl["fluxns"], t)
cmd := cmdTestCase{
args: "diff kustomization new-namespace-and-configmap --path ./testdata/build-kustomization/new-namespace-and-configmap --progress-bar=false " +
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml " +
"--ignore-not-found" +
" -n " + tmpl["fluxns"],
assert: assertError("ConfigMap/new-ns/app-config not found: namespaces \"new-ns\" not found"),
}
cmd.runTestCmd(t)
}
// TestDiffKustomizationNewNamespaceOnly runs `flux diff ks` when the
// kustomization creates only a new namespace. The diff should show the
// namespace as created.
func TestDiffKustomizationNewNamespaceOnly(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupTestNamespace(tmpl["fluxns"], t)
cmd := cmdTestCase{
args: "diff kustomization new-namespace-only --path ./testdata/build-kustomization/new-namespace-only --progress-bar=false " +
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml " +
"--ignore-not-found" +
" -n " + tmpl["fluxns"],
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-namespace-only.golden"),
}
cmd.runTestCmd(t)
}
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured { func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
buf, err := os.ReadFile(objectFile) buf, err := os.ReadFile(objectFile)
if err != nil { if err != nil {
+2 -5
View File
@@ -196,14 +196,11 @@ 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 { func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
listOpts := &metav1.ListOptions{} listOpts := &metav1.ListOptions{}
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
err := runtimeresource.FollowContinue(listOpts, err := runtimeresource.FollowContinue(listOpts,
func(options metav1.ListOptions) (runtime.Object, error) { func(options metav1.ListOptions) (runtime.Object, error) {
newEvents := &corev1.EventList{} newEvents := &corev1.EventList{}
opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize)) if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
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) return nil, fmt.Errorf("error getting events: %w", err)
} }
el.Items = append(el.Items, newEvents.Items...) el.Items = append(el.Items, newEvents.Items...)
-104
View File
@@ -20,13 +20,11 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "strings"
"testing" "testing"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@@ -421,108 +419,6 @@ 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 { func kindNameIndexer(obj client.Object) []string {
e, ok := obj.(*corev1.Event) e, ok := obj.(*corev1.Event)
if !ok { if !ok {
-84
View File
@@ -1,84 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
var exportSourceExternalCmd = &cobra.Command{
Use: "external [name]",
Short: "Export ExternalArtifact sources in YAML format",
Long: "The export source external command exports one or all ExternalArtifact sources in YAML format.",
Example: ` # Export all ExternalArtifact sources
flux export source external --all > sources.yaml
# Export a specific ExternalArtifact
flux export source external my-artifact > source.yaml`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
RunE: exportWithSecretCommand{
list: externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
object: externalArtifactAdapter{&sourcev1.ExternalArtifact{}},
}.run,
}
func init() {
exportSourceCmd.AddCommand(exportSourceExternalCmd)
}
func exportExternalArtifact(source *sourcev1.ExternalArtifact) any {
gvk := sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)
export := sourcev1.ExternalArtifact{
TypeMeta: metav1.TypeMeta{
Kind: gvk.Kind,
APIVersion: gvk.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: source.Name,
Namespace: source.Namespace,
Labels: source.Labels,
Annotations: source.Annotations,
},
Spec: source.Spec,
}
return export
}
func getExternalArtifactSecret(source *sourcev1.ExternalArtifact) *types.NamespacedName {
// ExternalArtifact does not have a secretRef in its spec, this satisfies the interface
return nil
}
func (ex externalArtifactAdapter) secret() *types.NamespacedName {
return getExternalArtifactSecret(ex.ExternalArtifact)
}
func (ex externalArtifactListAdapter) secretItem(i int) *types.NamespacedName {
return getExternalArtifactSecret(&ex.ExternalArtifactList.Items[i])
}
func (ex externalArtifactAdapter) export() any {
return exportExternalArtifact(ex.ExternalArtifact)
}
func (ex externalArtifactListAdapter) exportItem(i int) any {
return exportExternalArtifact(&ex.ExternalArtifactList.Items[i])
}
-6
View File
@@ -110,12 +110,6 @@ func TestExport(t *testing.T) {
"testdata/export/bucket.yaml", "testdata/export/bucket.yaml",
tmpl, tmpl,
}, },
{
"source external",
"export source external flux-system",
"testdata/export/external-artifact.yaml",
tmpl,
},
} }
for _, tt := range cases { for _, tt := range cases {
+3 -42
View File
@@ -28,22 +28,13 @@ import (
helmv2 "github.com/fluxcd/helm-controller/api/v2" helmv2 "github.com/fluxcd/helm-controller/api/v2"
) )
type getHelmReleaseFlags struct {
showSource bool
}
var getHrArgs getHelmReleaseFlags
var getHelmReleaseCmd = &cobra.Command{ var getHelmReleaseCmd = &cobra.Command{
Use: "helmreleases", Use: "helmreleases",
Aliases: []string{"hr", "helmrelease"}, Aliases: []string{"hr", "helmrelease"},
Short: "Get HelmRelease statuses", Short: "Get HelmRelease statuses",
Long: "The get helmreleases command prints the statuses of the resources.", Long: "The get helmreleases command prints the statuses of the resources.",
Example: ` # List all Helm releases and their status Example: ` # List all Helm releases and their status
flux get helmreleases flux get helmreleases`,
# List all Helm releases with source information
flux get helmreleases --show-source`,
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
get := getCommand{ get := getCommand{
@@ -78,7 +69,6 @@ var getHelmReleaseCmd = &cobra.Command{
} }
func init() { func init() {
getHelmReleaseCmd.Flags().BoolVar(&getHrArgs.showSource, "show-source", false, "show the source reference for each helmrelease")
getCmd.AddCommand(getHelmReleaseCmd) getCmd.AddCommand(getHelmReleaseCmd)
} }
@@ -89,45 +79,16 @@ func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string {
return helmRelease.Status.LastAttemptedRevision return helmRelease.Status.LastAttemptedRevision
} }
func getHelmReleaseSource(item helmv2.HelmRelease) string {
if item.Spec.ChartRef != nil {
ns := item.Spec.ChartRef.Namespace
if ns == "" {
ns = item.GetNamespace()
}
return fmt.Sprintf("%s/%s/%s",
item.Spec.ChartRef.Kind,
ns,
item.Spec.ChartRef.Name)
}
ns := item.Spec.Chart.Spec.SourceRef.Namespace
if ns == "" {
ns = item.GetNamespace()
}
return fmt.Sprintf("%s/%s/%s",
item.Spec.Chart.Spec.SourceRef.Kind,
ns,
item.Spec.Chart.Spec.SourceRef.Name)
}
func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
item := a.Items[i] item := a.Items[i]
revision := getHelmReleaseRevision(item) revision := getHelmReleaseRevision(item)
status, msg := statusAndMessage(item.Status.Conditions) status, msg := statusAndMessage(item.Status.Conditions)
row := nameColumns(&item, includeNamespace, includeKind) return append(nameColumns(&item, includeNamespace, includeKind),
if getHrArgs.showSource {
row = append(row, getHelmReleaseSource(item))
}
return append(row,
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string { func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {
headers := []string{"Name"} headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
if getHrArgs.showSource {
headers = append(headers, "Source")
}
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
if includeNamespace { if includeNamespace {
headers = append([]string{"Namespace"}, headers...) headers = append([]string{"Namespace"}, headers...)
} }
+3 -28
View File
@@ -30,22 +30,13 @@ import (
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
type getKustomizationFlags struct {
showSource bool
}
var getKsArgs getKustomizationFlags
var getKsCmd = &cobra.Command{ var getKsCmd = &cobra.Command{
Use: "kustomizations", Use: "kustomizations",
Aliases: []string{"ks", "kustomization"}, Aliases: []string{"ks", "kustomization"},
Short: "Get Kustomization statuses", Short: "Get Kustomization statuses",
Long: `The get kustomizations command prints the statuses of the resources.`, Long: `The get kustomizations command prints the statuses of the resources.`,
Example: ` # List all kustomizations and their status Example: ` # List all kustomizations and their status
flux get kustomizations flux get kustomizations`,
# List all kustomizations with source information
flux get kustomizations --show-source`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
get := getCommand{ get := getCommand{
@@ -83,7 +74,6 @@ var getKsCmd = &cobra.Command{
} }
func init() { func init() {
getKsCmd.Flags().BoolVar(&getKsArgs.showSource, "show-source", false, "show the source reference for each kustomization")
getCmd.AddCommand(getKsCmd) getCmd.AddCommand(getKsCmd)
} }
@@ -93,27 +83,12 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in
status, msg := statusAndMessage(item.Status.Conditions) status, msg := statusAndMessage(item.Status.Conditions)
revision = utils.TruncateHex(revision) revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
row := nameColumns(&item, includeNamespace, includeKind) return append(nameColumns(&item, includeNamespace, includeKind),
if getKsArgs.showSource {
sourceNs := item.Spec.SourceRef.Namespace
if sourceNs == "" {
sourceNs = item.GetNamespace()
}
row = append(row, fmt.Sprintf("%s/%s/%s",
item.Spec.SourceRef.Kind,
sourceNs,
item.Spec.SourceRef.Name))
}
return append(row,
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a kustomizationListAdapter) headers(includeNamespace bool) []string { func (a kustomizationListAdapter) headers(includeNamespace bool) []string {
headers := []string{"Name"} headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
if getKsArgs.showSource {
headers = append(headers, "Source")
}
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
if includeNamespace { if includeNamespace {
headers = append([]string{"Namespace"}, headers...) headers = append([]string{"Namespace"}, headers...)
} }
-4
View File
@@ -59,10 +59,6 @@ var getSourceAllCmd = &cobra.Command{
apiType: helmChartType, apiType: helmChartType,
list: &helmChartListAdapter{&sourcev1.HelmChartList{}}, list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
}, },
{
apiType: externalArtifactType,
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
},
} }
for _, c := range allSourceCmd { for _, c := range allSourceCmd {
-108
View File
@@ -1,108 +0,0 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/utils"
)
var getSourceExternalCmd = &cobra.Command{
Use: "external",
Short: "Get ExternalArtifact source statuses",
Long: `The get sources external command prints the status of the ExternalArtifact sources.`,
Example: ` # List all ExternalArtifacts and their status
flux get sources external
# List ExternalArtifacts from all namespaces
flux get sources external --all-namespaces`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
RunE: func(cmd *cobra.Command, args []string) error {
get := getCommand{
apiType: externalArtifactType,
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
funcMap: make(typeMap),
}
err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) {
o, ok := obj.(*sourcev1.ExternalArtifact)
if !ok {
return nil, fmt.Errorf("impossible to cast type %#v to ExternalArtifact", obj)
}
sink := &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{
Items: []sourcev1.ExternalArtifact{
*o,
}}}
return sink, nil
})
if err != nil {
return err
}
if err := get.run(cmd, args); err != nil {
return err
}
return nil
},
}
func init() {
getSourceCmd.AddCommand(getSourceExternalCmd)
}
func (a *externalArtifactListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
item := a.Items[i]
var revision string
if item.Status.Artifact != nil {
revision = item.Status.Artifact.Revision
}
status, msg := statusAndMessage(item.Status.Conditions)
revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg)
var source string
if item.Spec.SourceRef != nil {
source = fmt.Sprintf("%s/%s/%s",
item.Spec.SourceRef.Kind,
item.Spec.SourceRef.Namespace,
item.Spec.SourceRef.Name)
}
return append(nameColumns(&item, includeNamespace, includeKind),
revision, source, status, msg)
}
func (a externalArtifactListAdapter) headers(includeNamespace bool) []string {
headers := []string{"Name", "Revision", "Source", "Ready", "Message"}
if includeNamespace {
headers = append([]string{"Namespace"}, headers...)
}
return headers
}
func (a externalArtifactListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
item := a.Items[i]
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
}
+6 -43
View File
@@ -100,16 +100,6 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
# Uninstall Flux and delete CRDs # Uninstall Flux and delete CRDs
flux uninstall`, flux uninstall`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// If opted in via --ns-follows-kube-context flag or
// FLUX_NS_FOLLOWS_KUBE_CONTEXT env var, and --namespace was not
// explicitly set, respect the namespace from the kubeconfig context.
if !cmd.Flags().Changed("namespace") &&
(rootArgs.nsFollowsKubeContext || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "") {
if ctxNs := getKubeconfigContextNamespace(kubeconfigArgs); ctxNs != "" {
*kubeconfigArgs.Namespace = ctxNs
}
}
ns, err := cmd.Flags().GetString("namespace") ns, err := cmd.Flags().GetString("namespace")
if err != nil { if err != nil {
return fmt.Errorf("error getting namespace: %w", err) return fmt.Errorf("error getting namespace: %w", err)
@@ -126,11 +116,10 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
var logger = stderrLogger{stderr: os.Stderr} var logger = stderrLogger{stderr: os.Stderr}
type rootFlags struct { type rootFlags struct {
timeout time.Duration timeout time.Duration
verbose bool verbose bool
pollInterval time.Duration pollInterval time.Duration
nsFollowsKubeContext bool defaults install.Options
defaults install.Options
} }
// RequestError is a custom error type that wraps an error returned by the flux api. // RequestError is a custom error type that wraps an error returned by the flux api.
@@ -150,8 +139,6 @@ var kubeclientOptions = new(runclient.Options)
func init() { func init() {
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation") rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects") rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
rootCmd.PersistentFlags().BoolVar(&rootArgs.nsFollowsKubeContext, "ns-follows-kube-context", false,
"use the namespace from the kubeconfig context instead of the default flux-system namespace, can also be set via FLUX_NS_FOLLOWS_KUBE_CONTEXT env var")
configureDefaultNamespace() configureDefaultNamespace()
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
@@ -193,14 +180,12 @@ func main() {
// This is required because controller-runtime expects its consumers to // This is required because controller-runtime expects its consumers to
// set a logger through log.SetLogger within 30 seconds of the program's // set a logger through log.SetLogger within 30 seconds of the program's
// initialization. If not set, the entire debug stack is printed as an // initalization. If not set, the entire debug stack is printed as an
// error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59 // error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59
// Since we have our own logging and don't care about controller-runtime's // Since we have our own logging and don't care about controller-runtime's
// logger, we configure it's logger to do nothing. // logger, we configure it's logger to do nothing.
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{})) ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
registerPlugins()
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
if err, ok := err.(*RequestError); ok { if err, ok := err.(*RequestError); ok {
@@ -218,26 +203,6 @@ func main() {
} }
} }
// getKubeconfigContextNamespace returns the namespace from the current
// kubeconfig context, or an empty string if it cannot be determined.
func getKubeconfigContextNamespace(cf *genericclioptions.ConfigFlags) string {
rawConfig, err := cf.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return ""
}
currentContext := rawConfig.CurrentContext
if cf.Context != nil && *cf.Context != "" {
currentContext = *cf.Context
}
if ctx, ok := rawConfig.Contexts[currentContext]; ok {
return ctx.Namespace
}
return ""
}
func configureDefaultNamespace() { func configureDefaultNamespace() {
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE") fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
@@ -260,9 +225,7 @@ func configureDefaultNamespace() {
func readPasswordFromStdin(prompt string) (string, error) { func readPasswordFromStdin(prompt string) (string, error) {
var out string var out string
var err error var err error
if _, err := fmt.Fprint(os.Stdout, prompt); err != nil { fmt.Fprint(os.Stdout, prompt)
return "", fmt.Errorf("failed to write prompt: %w", err)
}
stdinFD := int(os.Stdin.Fd()) stdinFD := int(os.Stdin.Fd())
if term.IsTerminal(stdinFD) { if term.IsTerminal(stdinFD) {
var inBytes []byte var inBytes []byte
-221
View File
@@ -1,221 +0,0 @@
/*
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 (
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
func TestGetKubeconfigContextNamespace(t *testing.T) {
tests := []struct {
name string
kubeconfig string
context string
expectedResult string
}{
{
name: "returns namespace from current context",
kubeconfig: `apiVersion: v1
kind: Config
current-context: my-context
contexts:
- name: my-context
context:
cluster: my-cluster
namespace: custom-ns
clusters:
- name: my-cluster
cluster:
server: https://localhost:6443
`,
expectedResult: "custom-ns",
},
{
name: "returns empty when context has no namespace",
kubeconfig: `apiVersion: v1
kind: Config
current-context: my-context
contexts:
- name: my-context
context:
cluster: my-cluster
clusters:
- name: my-cluster
cluster:
server: https://localhost:6443
`,
expectedResult: "",
},
{
name: "returns namespace from context specified via --context flag",
kubeconfig: `apiVersion: v1
kind: Config
current-context: default-context
contexts:
- name: default-context
context:
cluster: my-cluster
namespace: default-ns
- name: other-context
context:
cluster: my-cluster
namespace: other-ns
clusters:
- name: my-cluster
cluster:
server: https://localhost:6443
`,
context: "other-context",
expectedResult: "other-ns",
},
{
name: "returns empty when context does not exist",
kubeconfig: `apiVersion: v1
kind: Config
current-context: non-existent
contexts: []
clusters: []
`,
expectedResult: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
// Write temporary kubeconfig.
tmpDir := t.TempDir()
kcPath := filepath.Join(tmpDir, "kubeconfig")
g.Expect(os.WriteFile(kcPath, []byte(tt.kubeconfig), 0o600)).To(Succeed())
// Use a local ConfigFlags instance to avoid polluting the
// package-global kubeconfigArgs (which caches a clientConfig
// internally and would leak state across tests).
cf := genericclioptions.NewConfigFlags(false)
cf.KubeConfig = &kcPath
cf.Context = &tt.context
got := getKubeconfigContextNamespace(cf)
g.Expect(got).To(Equal(tt.expectedResult))
})
}
}
func TestContextNamespaceOptIn(t *testing.T) {
kubeconfig := `apiVersion: v1
kind: Config
current-context: my-context
contexts:
- name: my-context
context:
cluster: my-cluster
namespace: context-ns
clusters:
- name: my-cluster
cluster:
server: https://localhost:6443
`
tests := []struct {
name string
nsFollowsFlag bool
nsFollowsEnv string
envNamespace string
flagNamespace string
expectedNamespace string
}{
{
name: "ignores context namespace when not opted in",
expectedNamespace: rootArgs.defaults.Namespace,
},
{
name: "uses context namespace when opted in via flag",
nsFollowsFlag: true,
expectedNamespace: "context-ns",
},
{
name: "uses context namespace when opted in via env var",
nsFollowsEnv: "1",
expectedNamespace: "context-ns",
},
{
name: "context namespace takes precedence over FLUX_SYSTEM_NAMESPACE when opted in",
nsFollowsFlag: true,
envNamespace: "env-ns",
expectedNamespace: "context-ns",
},
{
name: "FLUX_SYSTEM_NAMESPACE used when not opted in",
envNamespace: "env-ns",
expectedNamespace: "env-ns",
},
{
name: "--namespace flag takes precedence over context namespace",
nsFollowsFlag: true,
flagNamespace: "flag-ns",
expectedNamespace: "flag-ns",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
// Write temporary kubeconfig.
tmpDir := t.TempDir()
kcPath := filepath.Join(tmpDir, "kubeconfig")
g.Expect(os.WriteFile(kcPath, []byte(kubeconfig), 0o600)).To(Succeed())
// Use a local ConfigFlags instance to avoid polluting the
// package-global kubeconfigArgs.
cf := genericclioptions.NewConfigFlags(false)
cf.KubeConfig = &kcPath
emptyCtx := ""
cf.Context = &emptyCtx
// Mirror configureDefaultNamespace behavior on the local instance.
defaultNs := rootArgs.defaults.Namespace
cf.Namespace = &defaultNs
if tt.envNamespace != "" {
t.Setenv("FLUX_SYSTEM_NAMESPACE", tt.envNamespace)
envNs := tt.envNamespace
cf.Namespace = &envNs
}
if tt.nsFollowsEnv != "" {
t.Setenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT", tt.nsFollowsEnv)
}
// Simulate PersistentPreRunE behavior.
if tt.flagNamespace != "" {
*cf.Namespace = tt.flagNamespace
} else if tt.nsFollowsFlag || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "" {
if ctxNs := getKubeconfigContextNamespace(cf); ctxNs != "" {
*cf.Namespace = ctxNs
}
}
g.Expect(*cf.Namespace).To(Equal(tt.expectedNamespace))
})
}
}
-8
View File
@@ -374,12 +374,6 @@ func executeCommand(cmd string) (string, error) {
// in subsequent executions which causes tests to fail that rely on the value // in subsequent executions which causes tests to fail that rely on the value
// of "Changed". // of "Changed".
resumeCmd.PersistentFlags().Lookup("wait").Changed = false resumeCmd.PersistentFlags().Lookup("wait").Changed = false
// Reset the help flag value and Changed state so that a prior
// "--help" invocation does not leak into subsequent test runs.
if hf := rootCmd.Flags().Lookup("help"); hf != nil {
hf.Value.Set("false")
hf.Changed = false
}
}() }()
args, err := shellwords.Parse(cmd) args, err := shellwords.Parse(cmd)
if err != nil { if err != nil {
@@ -460,9 +454,7 @@ func resetCmdArgs() {
rhrArgs = reconcileHelmReleaseFlags{} rhrArgs = reconcileHelmReleaseFlags{}
rksArgs = reconcileKsFlags{} rksArgs = reconcileKsFlags{}
secretGitArgs = NewSecretGitFlags() secretGitArgs = NewSecretGitFlags()
secretGitHubAppArgs = secretGitHubAppFlags{}
secretProxyArgs = secretProxyFlags{} secretProxyArgs = secretProxyFlags{}
secretReceiverArgs = secretReceiverFlags{}
secretHelmArgs = secretHelmFlags{} secretHelmArgs = secretHelmFlags{}
secretTLSArgs = secretTLSFlags{} secretTLSArgs = secretTLSFlags{}
sourceBucketArgs = sourceBucketFlags{} sourceBucketArgs = sourceBucketFlags{}
+60 -71
View File
@@ -59,82 +59,71 @@ type APIVersions struct {
// TODO: Update this mapping when new Flux minor versions are released! // TODO: Update this mapping when new Flux minor versions are released!
// latestAPIVersions contains the latest API versions for each GroupKind // latestAPIVersions contains the latest API versions for each GroupKind
// for each supported Flux version. The number of latest minor versions // for each supported Flux version. We maintain the latest two minor versions.
// we maintain here must match what's documented here:
//
// https://fluxcd.io/flux/releases/#supported-releases
var latestAPIVersions = []APIVersions{ var latestAPIVersions = []APIVersions{
{ {
FluxVersion: "2.8", FluxVersion: "2.7",
LatestVersions: flux27LatestAPIVersions, 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.7", FluxVersion: "2.6",
LatestVersions: flux27LatestAPIVersions, 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.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{ var migrateCmd = &cobra.Command{
-118
View File
@@ -1,118 +0,0 @@
/*
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"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/plugin"
)
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
},
}
func init() {
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)
}
}
// 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, ""
}
// isDigestRef reports whether ref is a content-addressable digest
// (e.g. "sha256:06e0a38...").
func isDigestRef(ref string) bool {
return strings.HasPrefix(ref, "sha256:")
}
// 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
}
-96
View File
@@ -1,96 +0,0 @@
/*
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"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/plugin"
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
)
var pluginInstallCmd = &cobra.Command{
Use: "install <name>[@<version>|@<digest>]",
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
# Install pinned to a specific digest
flux plugin install operator@sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a`,
Args: cobra.ExactArgs(1),
RunE: pluginInstallCmdRun,
}
func init() {
pluginCmd.AddCommand(pluginInstallCmd)
}
func pluginInstallCmdRun(cmd *cobra.Command, args []string) error {
nameVersion := args[0]
name, ref := parseNameVersion(nameVersion)
catalogClient := newCatalogClient()
manifest, err := catalogClient.FetchManifest(name)
if err != nil {
return err
}
var pv *plugintypes.Version
var plat *plugintypes.Platform
if isDigestRef(ref) {
dm, err := plugin.ResolveByDigest(manifest, ref, runtime.GOOS, runtime.GOARCH)
if err != nil {
return err
}
pv = dm.Version
plat = dm.Platform
} else {
pv, err = plugin.ResolveVersion(manifest, ref)
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
}
-57
View File
@@ -1,57 +0,0 @@
/*
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 (
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/plugin"
"github.com/fluxcd/flux2/v2/pkg/printers"
)
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,
}
func init() {
pluginCmd.AddCommand(pluginListCmd)
}
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)
}
-81
View File
@@ -1,81 +0,0 @@
/*
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 (
"strings"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/plugin"
"github.com/fluxcd/flux2/v2/pkg/printers"
)
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(pluginSearchCmd)
}
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)
}
-286
View File
@@ -1,286 +0,0 @@
/*
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
}
}
}()
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", ""},
{"operator@sha256:abc123", "operator", "sha256:abc123"},
}
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 TestIsDigestRef(t *testing.T) {
tests := []struct {
input string
want bool
}{
{"sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a", true},
{"0.45.0", false},
{"", false},
{"sha256", false},
{"SHA256:abc", false}, // case-sensitive
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
if got := isDigestRef(tt.input); got != tt.want {
t.Errorf("isDigestRef(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
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)
}
}
-48
View File
@@ -1,48 +0,0 @@
/*
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 (
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/plugin"
)
var pluginUninstallCmd = &cobra.Command{
Use: "uninstall <name>",
Aliases: []string{"delete"},
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,
}
func init() {
pluginCmd.AddCommand(pluginUninstallCmd)
}
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
}
-102
View File
@@ -1,102 +0,0 @@
/*
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"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/plugin"
)
var pluginUpdateCmd = &cobra.Command{
Use: "update [name]",
Aliases: []string{"upgrade"},
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,
}
func init() {
pluginCmd.AddCommand(pluginUpdateCmd)
}
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
}
+11 -22
View File
@@ -103,18 +103,17 @@ The command can read the credentials from '~/.docker/config.json' but they can a
} }
type pushArtifactFlags struct { type pushArtifactFlags struct {
path string path string
source string source string
revision string revision string
creds string creds string
provider flags.SourceOCIProvider provider flags.SourceOCIProvider
ignorePaths []string ignorePaths []string
annotations []string annotations []string
output string output string
debug bool debug bool
reproducible bool reproducible bool
insecure bool insecure bool
resolveSymlinks bool
} }
var pushArtifactArgs = newPushArtifactFlags() var pushArtifactArgs = newPushArtifactFlags()
@@ -138,7 +137,6 @@ func init() {
pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library") pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library")
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'") pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'")
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS") pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS")
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
pushCmd.AddCommand(pushArtifactCmd) pushCmd.AddCommand(pushArtifactCmd)
} }
@@ -185,15 +183,6 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err) return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err)
} }
if pushArtifactArgs.resolveSymlinks {
resolved, cleanupDir, err := resolveSymlinks(path)
if err != nil {
return fmt.Errorf("resolving symlinks failed: %w", err)
}
defer os.RemoveAll(cleanupDir)
path = resolved
}
annotations := map[string]string{} annotations := map[string]string{}
for _, annotation := range pushArtifactArgs.annotations { for _, annotation := range pushArtifactArgs.annotations {
kv := strings.Split(annotation, "=") kv := strings.Split(annotation, "=")
+1 -8
View File
@@ -152,14 +152,7 @@ func reconciliationHandled(kubeClient client.Client, namespacedName types.Namesp
return false, err return false, err
} }
switch result.Status { return result.Status == kstatus.CurrentStatus, nil
case kstatus.CurrentStatus:
return true, nil
case kstatus.InProgressStatus:
return false, nil
default:
return false, fmt.Errorf("%s", result.Message)
}
} }
} }
+14 -19
View File
@@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
helmv2 "github.com/fluxcd/helm-controller/api/v2" helmv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
@@ -66,7 +67,7 @@ func (obj helmReleaseAdapter) reconcileSource() bool {
return rhrArgs.syncHrWithSource return rhrArgs.syncHrWithSource
} }
func (obj helmReleaseAdapter) getSource() (reconcileSource, sourceReference) { func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName) {
var ( var (
name string name string
ns string ns string
@@ -77,26 +78,21 @@ func (obj helmReleaseAdapter) getSource() (reconcileSource, sourceReference) {
if ns == "" { if ns == "" {
ns = obj.Namespace ns = obj.Namespace
} }
srcRef := sourceReference{ namespacedName := types.NamespacedName{
kind: obj.Spec.ChartRef.Kind, Name: name,
name: name, Namespace: ns,
namespace: ns,
} }
switch obj.Spec.ChartRef.Kind { if obj.Spec.ChartRef.Kind == sourcev1.HelmChartKind {
case sourcev1.HelmChartKind:
return reconcileWithSourceCommand{ return reconcileWithSourceCommand{
apiType: helmChartType, apiType: helmChartType,
object: helmChartAdapter{&sourcev1.HelmChart{}}, object: helmChartAdapter{&sourcev1.HelmChart{}},
force: true, force: true,
}, srcRef }, namespacedName
case sourcev1.OCIRepositoryKind:
return reconcileCommand{
apiType: ociRepositoryType,
object: ociRepositoryAdapter{&sourcev1.OCIRepository{}},
}, srcRef
default:
return nil, srcRef
} }
return reconcileCommand{
apiType: ociRepositoryType,
object: ociRepositoryAdapter{&sourcev1.OCIRepository{}},
}, namespacedName
default: default:
// default case assumes the HelmRelease is using a HelmChartTemplate // default case assumes the HelmRelease is using a HelmChartTemplate
ns = obj.Spec.Chart.Spec.SourceRef.Namespace ns = obj.Spec.Chart.Spec.SourceRef.Namespace
@@ -108,10 +104,9 @@ func (obj helmReleaseAdapter) getSource() (reconcileSource, sourceReference) {
apiType: helmChartType, apiType: helmChartType,
object: helmChartAdapter{&sourcev1.HelmChart{}}, object: helmChartAdapter{&sourcev1.HelmChart{}},
force: true, force: true,
}, sourceReference{ }, types.NamespacedName{
kind: sourcev1.HelmChartKind, Name: name,
name: name, Namespace: ns,
namespace: ns,
} }
} }
} }
+6 -6
View File
@@ -18,6 +18,7 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
@@ -61,8 +62,8 @@ func (obj kustomizationAdapter) reconcileSource() bool {
return rksArgs.syncKsWithSource return rksArgs.syncKsWithSource
} }
func (obj kustomizationAdapter) getSource() (reconcileSource, sourceReference) { func (obj kustomizationAdapter) getSource() (reconcileSource, types.NamespacedName) {
var cmd reconcileSource var cmd reconcileCommand
switch obj.Spec.SourceRef.Kind { switch obj.Spec.SourceRef.Kind {
case sourcev1.OCIRepositoryKind: case sourcev1.OCIRepositoryKind:
cmd = reconcileCommand{ cmd = reconcileCommand{
@@ -81,10 +82,9 @@ func (obj kustomizationAdapter) getSource() (reconcileSource, sourceReference) {
} }
} }
return cmd, sourceReference{ return cmd, types.NamespacedName{
kind: obj.Spec.SourceRef.Kind, Name: obj.Spec.SourceRef.Name,
name: obj.Spec.SourceRef.Name, Namespace: obj.Spec.SourceRef.Namespace,
namespace: obj.Spec.SourceRef.Namespace,
} }
} }
+6 -6
View File
@@ -18,6 +18,7 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
) )
@@ -57,8 +58,8 @@ func (obj helmChartAdapter) reconcileSource() bool {
return rhcArgs.syncHrWithSource return rhcArgs.syncHrWithSource
} }
func (obj helmChartAdapter) getSource() (reconcileSource, sourceReference) { func (obj helmChartAdapter) getSource() (reconcileSource, types.NamespacedName) {
var cmd reconcileSource var cmd reconcileCommand
switch obj.Spec.SourceRef.Kind { switch obj.Spec.SourceRef.Kind {
case sourcev1.HelmRepositoryKind: case sourcev1.HelmRepositoryKind:
cmd = reconcileCommand{ cmd = reconcileCommand{
@@ -77,10 +78,9 @@ func (obj helmChartAdapter) getSource() (reconcileSource, sourceReference) {
} }
} }
return cmd, sourceReference{ return cmd, types.NamespacedName{
kind: obj.Spec.SourceRef.Kind, Name: obj.Spec.SourceRef.Name,
name: obj.Spec.SourceRef.Name, Namespace: obj.Namespace,
namespace: obj.Namespace,
} }
} }
+6 -15
View File
@@ -15,17 +15,11 @@ import (
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
type sourceReference struct {
kind string
name string
namespace string
}
type reconcileWithSource interface { type reconcileWithSource interface {
adapter adapter
reconcilable reconcilable
reconcileSource() bool reconcileSource() bool
getSource() (reconcileSource, sourceReference) getSource() (reconcileSource, types.NamespacedName)
} }
type reconcileSource interface { type reconcileSource interface {
@@ -67,17 +61,14 @@ func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []strin
} }
if reconcile.object.reconcileSource() || reconcile.force { if reconcile.object.reconcileSource() || reconcile.force {
reconcileCmd, srcRef := reconcile.object.getSource() reconcileCmd, nsName := reconcile.object.getSource()
if reconcileCmd == nil {
return fmt.Errorf("cannot reconcile source of kind %s", srcRef.kind)
}
nsCopy := *kubeconfigArgs.Namespace nsCopy := *kubeconfigArgs.Namespace
if srcRef.namespace != "" { if nsName.Namespace != "" {
*kubeconfigArgs.Namespace = srcRef.namespace *kubeconfigArgs.Namespace = nsName.Namespace
} }
if err := reconcileCmd.run(nil, []string{srcRef.name}); err != nil { err := reconcileCmd.run(nil, []string{nsName.Name})
if err != nil {
return err return err
} }
*kubeconfigArgs.Namespace = nsCopy *kubeconfigArgs.Namespace = nsCopy
+1 -13
View File
@@ -126,17 +126,6 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error {
resume.printMessage(reconcileResps) 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 return nil
} }
@@ -262,8 +251,7 @@ func (resume resumeCommand) printMessage(responses []reconcileResponse) {
continue continue
} }
if r.err != nil { if r.err != nil {
logger.Failuref("%s %s reconciliation failed: %s", resume.kind, r.asClientObject().GetName(), r.err.Error()) logger.Failuref("%s", r.err.Error())
continue
} }
logger.Successf("%s %s reconciliation completed", resume.kind, r.asClientObject().GetName()) logger.Successf("%s %s reconciliation completed", resume.kind, r.asClientObject().GetName())
logger.Successf("%s", r.successMessage()) logger.Successf("%s", r.successMessage())
-34
View File
@@ -195,37 +195,3 @@ func (a helmRepositoryListAdapter) asClientList() client.ObjectList {
func (a helmRepositoryListAdapter) len() int { func (a helmRepositoryListAdapter) len() int {
return len(a.HelmRepositoryList.Items) return len(a.HelmRepositoryList.Items)
} }
// sourcev1.ExternalArtifact
var externalArtifactType = apiType{
kind: sourcev1.ExternalArtifactKind,
humanKind: "source external-artifact",
groupVersion: sourcev1.GroupVersion,
}
type externalArtifactAdapter struct {
*sourcev1.ExternalArtifact
}
func (a externalArtifactAdapter) asClientObject() client.Object {
return a.ExternalArtifact
}
func (a externalArtifactAdapter) deepCopyClientObject() client.Object {
return a.ExternalArtifact.DeepCopy()
}
// sourcev1.ExternalArtifactList
type externalArtifactListAdapter struct {
*sourcev1.ExternalArtifactList
}
func (a externalArtifactListAdapter) asClientList() client.ObjectList {
return a.ExternalArtifactList
}
func (a externalArtifactListAdapter) len() int {
return len(a.ExternalArtifactList.Items)
}
-8
View File
@@ -1,8 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDuUiEMA0
eUvKlmOsur2w9FAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIDF/w86ZQb5qmZtv
m1GvyLojiJdhmPtI9hJ9XPcP7HBoAAAAkG2cIOuSVdWInSC0P81ExiorUpiAGOjxxpgvKW
VYERfU1zU72Z/c9n1+z/IH5cJOhZ1vlqBO0rubl4s0KQFvY/LKcsc4N0x0uzpqrvcJP4tO
9VW8LrMnrPp7b6KVJPsbeSW1SBcUM24aCMzF4/wV03mN/Uqz30s+YgS9SU4Lz8AOkX58xX
yAV0gkmndIzZl+Og==
-----END OPENSSH PRIVATE KEY-----
-7
View File
@@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCAAAAIjjSDmx40g5
sQAAAAtzc2gtZWQyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCA
AAAEAGpzSFuLkCNDD49+tysxSFFwdOsRnDj67vDT9bfwoSDhYOV20IV1IxchXW8vBe9HCT
h4SZKgMKnE2Rxs2mHd0IAAAABHRlc3QB
-----END OPENSSH PRIVATE KEY-----
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
not a real ssh key
@@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: existing-config
namespace: default
data:
key: value
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./existing.yaml
- ./new.yaml
@@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: new-config
namespace: default
data:
key: value
@@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: new-ns
data:
key: value
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./namespace.yaml
- ./configmap.yaml
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: new-ns
@@ -1,4 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./namespace.yaml
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: new-ns
+1 -1
View File
@@ -1,3 +1,3 @@
► checking prerequisites ► checking prerequisites
✔ Kubernetes {{ .serverVersion }} >=1.33.0-0 ✔ Kubernetes {{ .serverVersion }} >=1.32.0-0
✔ prerequisites checks passed ✔ prerequisites checks passed
-19
View File
@@ -1,19 +0,0 @@
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
git:
checkout:
ref:
branch: main
commit:
author:
email: flux@example.com
name: flux
interval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system
@@ -1,23 +0,0 @@
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
git:
checkout:
ref:
branch: main
commit:
author:
email: flux@example.com
name: flux
signingKey:
secretRef:
name: my-key
type: gpg
interval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system
-23
View File
@@ -1,23 +0,0 @@
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
name: flux-system
namespace: flux-system
spec:
git:
checkout:
ref:
branch: main
commit:
author:
email: flux@example.com
name: flux
signingKey:
secretRef:
name: my-deploy-key
type: ssh
interval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system
+1 -1
View File
@@ -6,7 +6,7 @@ metadata:
namespace: my-namespace namespace: my-namespace
stringData: stringData:
githubAppID: "1" githubAppID: "1"
githubAppInstallationOwner: my-org githubAppInstallationID: "2"
githubAppPrivateKey: |- githubAppPrivateKey: |-
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap
@@ -1,13 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
annotations:
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
name: gcr-secret
namespace: my-namespace
stringData:
audience: https://custom.audience.example.com
email: sa@project.iam.gserviceaccount.com
token: test-token
@@ -1,13 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
annotations:
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
name: gcr-secret
namespace: my-namespace
stringData:
audience: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
email: sa@project.iam.gserviceaccount.com
token: test-token
@@ -1,11 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
annotations:
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/106120121d366c2f67e93200f6c1dbe938235eb588daa5e8c0516d3a77ac1dee
name: receiver-secret
namespace: my-namespace
stringData:
token: test-token
@@ -1,27 +0,0 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
toolkit.fluxcd.io/tenant: dev-team
name: dev-team
namespace: apps
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
toolkit.fluxcd.io/tenant: dev-team
name: dev-team-reconciler
namespace: apps
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: gotk:apps:reconciler
- kind: ServiceAccount
name: dev-team
namespace: apps

Some files were not shown because too many files have changed in this diff Show More