Compare commits

..

37 commits
main ... v2.8.8

Author SHA1 Message Date
Matheus Pimenta
1fd61a0626
Merge pull request #5904 from fluxcd/update-components-release/v2.8.x
Some checks failed
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
scan / analyze (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
Update toolkit components
2026-05-20 11:58:20 +01:00
fluxcdbot
477f048ec0 Update toolkit components
- helm-controller to v1.5.5
  https://github.com/fluxcd/helm-controller/blob/v1.5.5/CHANGELOG.md
- source-controller to v1.8.5
  https://github.com/fluxcd/source-controller/blob/v1.8.5/CHANGELOG.md
- image-reflector-controller to v1.1.2
  https://github.com/fluxcd/image-reflector-controller/blob/v1.1.2/CHANGELOG.md
- image-automation-controller to v1.1.4
  https://github.com/fluxcd/image-automation-controller/blob/v1.1.4/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
2026-05-20 10:44:14 +00:00
Matheus Pimenta
0acfaa26c6
Merge pull request #5899 from fluxcd/update-pkg-deps/release/v2.8.x
Some checks failed
scan / analyze (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
Update fluxcd/pkg dependencies
2026-05-19 13:44:33 +01:00
matheuscscp
264957f40b Update fluxcd/pkg dependencies
Signed-off-by: GitHub <noreply@github.com>
2026-05-19 12:23:39 +00:00
Matheus Pimenta
54e4ba378e
Merge pull request #5891 from fluxcd/update-components-release/v2.8.x
Some checks failed
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
scan / analyze (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
Update toolkit components
2026-05-12 12:01:49 +01:00
fluxcdbot
d2fbb16656 Update toolkit components
- kustomize-controller to v1.8.5
  https://github.com/fluxcd/kustomize-controller/blob/v1.8.5/CHANGELOG.md
- source-controller to v1.8.4
  https://github.com/fluxcd/source-controller/blob/v1.8.4/CHANGELOG.md
- image-automation-controller to v1.1.3
  https://github.com/fluxcd/image-automation-controller/blob/v1.1.3/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
2026-05-12 10:49:14 +00:00
Matheus Pimenta
66533d7c90
Merge pull request #5882 from fluxcd/backport-5881-to-release/v2.8.x
Some checks failed
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
scan / analyze (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
[release/v2.8.x] include source-watcher in install.yaml manifests
2026-05-06 16:50:02 +01:00
Thomas Morin
7ac36233f3 include source-watcher in install manifests
Signed-off-by: Thomas Morin <thomas.morin@orange.com>
(cherry picked from commit 626bb58a69)
2026-05-06 15:49:20 +00:00
Matheus Pimenta
04acaec616
Merge pull request #5857 from fluxcd/update-components-release/v2.8.x
Some checks failed
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
scan / analyze (push) Has been cancelled
Update toolkit components
2026-04-21 11:41:44 +01:00
fluxcdbot
f7dcb68bc0 Update toolkit components
- helm-controller to v1.5.4
  https://github.com/fluxcd/helm-controller/blob/v1.5.4/CHANGELOG.md
- kustomize-controller to v1.8.4
  https://github.com/fluxcd/kustomize-controller/blob/v1.8.4/CHANGELOG.md
- source-controller to v1.8.3
  https://github.com/fluxcd/source-controller/blob/v1.8.3/CHANGELOG.md
- notification-controller to v1.8.4
  https://github.com/fluxcd/notification-controller/blob/v1.8.4/CHANGELOG.md
- image-automation-controller to v1.1.2
  https://github.com/fluxcd/image-automation-controller/blob/v1.1.2/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
2026-04-21 10:27:04 +00:00
Stefan Prodan
56110c02f8
Merge pull request #5837 from fluxcd/backport-5835-to-release/v2.8.x
Some checks failed
scan / analyze (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
[release/v2.8.x] Add `flux create secret receiver` command
2026-04-10 13:34:24 +03:00
Stefan Prodan
e15e2d9a34 Add --audience-claim for GCR Receivers
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
(cherry picked from commit c601a212f6)
2026-04-10 10:12:46 +00:00
Stefan Prodan
bfd59e188f Add flux create secret receiver command
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
(cherry picked from commit 02734f28ba)
2026-04-10 10:12:46 +00:00
Stefan Prodan
5adad89dcc
Merge pull request #5822 from fluxcd/update-components-release/v2.8.x
Some checks failed
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
scan / analyze (push) Has been cancelled
Update toolkit components
2026-04-07 20:29:07 +03:00
fluxcdbot
29cc0b337e Update toolkit components
- kustomize-controller to v1.8.3
  https://github.com/fluxcd/kustomize-controller/blob/v1.8.3/CHANGELOG.md
- source-controller to v1.8.2
  https://github.com/fluxcd/source-controller/blob/v1.8.2/CHANGELOG.md
- notification-controller to v1.8.3
  https://github.com/fluxcd/notification-controller/blob/v1.8.3/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
2026-04-07 17:16:00 +00:00
Matheus Pimenta
3cf3cb546f
Merge pull request #5799 from fluxcd/backport-5798-to-release/v2.8.x
Some checks failed
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
scan / analyze (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
[release/v2.8.x] fix: validate --source flag in create kustomization command
2026-03-30 12:42:53 +01:00
Ghassan Malke
1569932e08 fix: validate --source flag in create kustomization command
Signed-off-by: Ghassan Malke <gmalke@shiftbase.com>
(cherry picked from commit 457abed9f9)
2026-03-30 11:41:27 +00:00
Matheus Pimenta
836f87ef8f
Merge pull request #5796 from fluxcd/update-pkg-deps/release/v2.8.x
Update fluxcd/pkg dependencies
2026-03-30 12:29:00 +01:00
matheuscscp
19545a8e46 Update fluxcd/pkg dependencies
Signed-off-by: GitHub <noreply@github.com>
2026-03-30 11:18:16 +00:00
Matheus Pimenta
871be9b40d
Merge pull request #5779 from fluxcd/update-components-release/v2.8.x
Some checks failed
scan / analyze (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
Update toolkit components
2026-03-16 13:39:29 +00:00
fluxcdbot
f7a168935d Update toolkit components
- helm-controller to v1.5.3
  https://github.com/fluxcd/helm-controller/blob/v1.5.3/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
2026-03-16 13:22:19 +00:00
Matheus Pimenta
bf67d7799d
Merge pull request #5774 from fluxcd/backport-5773-to-release/v2.8.x
Some checks failed
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
scan / analyze (push) Has been cancelled
[release/v2.8.x] Add target branch name to update branch
2026-03-12 16:38:13 +00:00
Matheus Pimenta
5cb2208cb7 Add target branch name to update branch
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
(cherry picked from commit 484346ffcc)
2026-03-12 16:37:19 +00:00
Matheus Pimenta
bfa461ed21
Merge pull request #5771 from fluxcd/update-pkg-deps/release/v2.8.x
Some checks are pending
conformance / conform-kubernetes (1.33.0) (push) Waiting to run
conformance / conform-kubernetes (1.34.1) (push) Waiting to run
conformance / conform-kubernetes (1.35.0) (push) Waiting to run
conformance / conform-k3s (1.33.7) (push) Waiting to run
conformance / conform-k3s (1.34.3) (push) Waiting to run
conformance / conform-k3s (1.35.0) (push) Waiting to run
conformance / conform-openshift (4.20.0-okd) (push) Waiting to run
e2e-bootstrap / e2e-boostrap-github (push) Waiting to run
e2e / e2e-amd64-kubernetes (push) Waiting to run
scan / analyze (push) Waiting to run
Update fluxcd/pkg dependencies
2026-03-12 14:26:38 +00:00
matheuscscp
f11a921e0c Update fluxcd/pkg dependencies
Signed-off-by: GitHub <noreply@github.com>
2026-03-12 14:24:51 +00:00
Matheus Pimenta
b248efab1d
Merge pull request #5770 from fluxcd/backport-5769-to-release/v2.8.x
Update toolkit components
2026-03-12 14:23:38 +00:00
fluxcdbot
4d5e044eb9 Update toolkit components
- helm-controller to v1.5.2
  https://github.com/fluxcd/helm-controller/blob/v1.5.2/CHANGELOG.md
- kustomize-controller to v1.8.2
  https://github.com/fluxcd/kustomize-controller/blob/v1.8.2/CHANGELOG.md
- source-controller to v1.8.1
  https://github.com/fluxcd/source-controller/blob/v1.8.1/CHANGELOG.md
- notification-controller to v1.8.2
  https://github.com/fluxcd/notification-controller/blob/v1.8.2/CHANGELOG.md
- image-reflector-controller to v1.1.1
  https://github.com/fluxcd/image-reflector-controller/blob/v1.1.1/CHANGELOG.md
- image-automation-controller to v1.1.1
  https://github.com/fluxcd/image-automation-controller/blob/v1.1.1/CHANGELOG.md
- source-watcher to v2.1.1
  https://github.com/fluxcd/source-watcher/blob/v2.1.1/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
(cherry picked from commit 2288dd90d6)
2026-03-12 14:19:08 +00:00
Matheus Pimenta
3c8917ca28
Merge pull request #5767 from fluxcd/update-pkg-deps/release/v2.8.x
Update fluxcd/pkg dependencies
2026-03-12 10:35:43 +00:00
matheuscscp
c1f11bcf3d Update fluxcd/pkg dependencies
Signed-off-by: GitHub <noreply@github.com>
2026-03-12 10:23:20 +00:00
Stefan Prodan
bc6aa4754c
Merge pull request #5765 from fluxcd/backport-5764-to-release/v2.8.x
Some checks are pending
conformance / conform-kubernetes (1.33.0) (push) Waiting to run
conformance / conform-kubernetes (1.34.1) (push) Waiting to run
conformance / conform-kubernetes (1.35.0) (push) Waiting to run
conformance / conform-k3s (1.33.7) (push) Waiting to run
conformance / conform-k3s (1.34.3) (push) Waiting to run
conformance / conform-k3s (1.35.0) (push) Waiting to run
conformance / conform-openshift (4.20.0-okd) (push) Waiting to run
e2e-bootstrap / e2e-boostrap-github (push) Waiting to run
e2e / e2e-amd64-kubernetes (push) Waiting to run
scan / analyze (push) Waiting to run
[release/v2.8.x] build(deps): bump the ci group across 1 directory with 11 updates
2026-03-11 19:22:44 +02:00
dependabot[bot]
0e7194ce77 build(deps): bump the ci group across 1 directory with 11 updates
Bumps the ci group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.2.0` | `6.3.0` |
| [replicatedhq/replicated-actions](https://github.com/replicatedhq/replicated-actions) | `1.19.0` | `1.20.0` |
| [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) | `3.1.2` | `4.0.0` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `3.7.0` | `4.0.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.12.0` | `4.0.0` |
| [docker/login-action](https://github.com/docker/login-action) | `3.7.0` | `4.0.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.32.4` | `4.32.6` |
| [anchore/sbom-action](https://github.com/anchore/sbom-action) | `0.22.2` | `0.23.1` |
| [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `4.0.0` | `4.1.0` |
| [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | `6.4.0` | `7.0.0` |

Updates `actions/setup-go` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](7a3fe6cf4c...4b73464bb3)

Updates `replicatedhq/replicated-actions` from 1.19.0 to 1.20.0
- [Release notes](https://github.com/replicatedhq/replicated-actions/releases)
- [Commits](49b440dabd...1abb33f527)

Updates `hashicorp/setup-terraform` from 3.1.2 to 4.0.0
- [Release notes](https://github.com/hashicorp/setup-terraform/releases)
- [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md)
- [Commits](b9cd54a3c3...5e8dbf3c6d)

Updates `docker/setup-qemu-action` from 3.7.0 to 4.0.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](c7c5346462...ce360397dd)

Updates `docker/setup-buildx-action` from 3.12.0 to 4.0.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](8d2750c68a...4d04d5d948)

Updates `docker/login-action` from 3.7.0 to 4.0.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c94ce9fb46...b45d80f862)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](b7c566a772...bbbca2ddaa)

Updates `github/codeql-action` from 4.32.4 to 4.32.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](89a39a4e59...0d579ffd05)

Updates `anchore/sbom-action` from 0.22.2 to 0.23.1
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](28d71544de...57aae52805)

Updates `sigstore/cosign-installer` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](faadad0cce...ba7bc0a3fe)

Updates `goreleaser/goreleaser-action` from 6.4.0 to 7.0.0
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](e435ccd777...ec59f474b9)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: replicatedhq/replicated-actions
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: hashicorp/setup-terraform
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: github/codeql-action
  dependency-version: 4.32.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci
- dependency-name: anchore/sbom-action
  dependency-version: 0.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit b60dfbe970)
2026-03-11 17:02:56 +00:00
Matheus Pimenta
97222a775a
Merge pull request #5741 from fluxcd/backport-5740-to-release/v2.8.x
Some checks failed
e2e / e2e-amd64-kubernetes (push) Has been cancelled
scan / analyze (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
[release/v2.8.x] Update toolkit components
2026-02-27 09:39:11 +00:00
fluxcdbot
959025e649 Update toolkit components
- helm-controller to v1.5.1
  https://github.com/fluxcd/helm-controller/blob/v1.5.1/CHANGELOG.md
- kustomize-controller to v1.8.1
  https://github.com/fluxcd/kustomize-controller/blob/v1.8.1/CHANGELOG.md
- notification-controller to v1.8.1
  https://github.com/fluxcd/notification-controller/blob/v1.8.1/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
(cherry picked from commit ab4bbffa5b)
2026-02-27 09:28:19 +00:00
Matheus Pimenta
75f5e33fd1
Merge pull request #5739 from fluxcd/update-pkg-deps/release/v2.8.x
Update fluxcd/pkg dependencies
2026-02-27 09:05:41 +00:00
matheuscscp
79cae5d033 Update fluxcd/pkg dependencies
Signed-off-by: GitHub <noreply@github.com>
2026-02-27 08:52:59 +00:00
Matheus Pimenta
41505fd5f3
Merge pull request #5735 from fluxcd/backport-5733-to-release/v2.8.x
Some checks failed
e2e-bootstrap / e2e-boostrap-github (push) Has been cancelled
e2e / e2e-amd64-kubernetes (push) Has been cancelled
conformance / conform-k3s (1.33.7) (push) Has been cancelled
conformance / conform-k3s (1.35.0) (push) Has been cancelled
conformance / conform-kubernetes (1.33.0) (push) Has been cancelled
conformance / conform-kubernetes (1.35.0) (push) Has been cancelled
conformance / conform-kubernetes (1.34.1) (push) Has been cancelled
conformance / conform-openshift (4.20.0-okd) (push) Has been cancelled
conformance / conform-k3s (1.34.3) (push) Has been cancelled
scan / analyze (push) Has been cancelled
[release/v2.8.x] Remove no longer needed workaround for Flux 2.8
2026-02-25 11:10:32 +00:00
Matheus Pimenta
2042ddf849 Remove no longer needed workaround for Flux 2.8
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
(cherry picked from commit 2666eaf8fc)
2026-02-25 11:00:30 +00:00
154 changed files with 1206 additions and 9152 deletions

12
.github/labels.yaml vendored
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.5.x
description: To be backported to release/v2.5.x
color: '#ffd700'
- name: backport:release/v2.6.x
description: To be backported to release/v2.6.x
color: '#ffd700'
- name: backport:release/v2.7.x - name: backport:release/v2.7.x
description: To be backported to release/v2.7.x description: To be backported to release/v2.7.x
color: '#ffd700' color: '#ffd700'
- name: backport:release/v2.8.x
description: To be backported to release/v2.8.x
color: '#ffd700'
- name: backport:release/v2.9.x
description: To be backported to release/v2.9.x
color: '#ffd700'

View file

@ -24,44 +24,6 @@ jobs:
name: action on ${{ matrix.version }} name: action on ${{ matrix.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup flux - name: Setup flux
uses: ./action uses: ./action
plugins:
runs-on: ubuntu-latest
name: action plugins on ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Build flux
shell: bash
run: |
set -euo pipefail
mkdir -p cmd/flux/manifests "$RUNNER_TEMP/flux-bin"
printf "apiVersion: v1\nkind: List\nitems: []\n" > cmd/flux/manifests/dummy.yaml
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=0.0.0-pr" -o "$RUNNER_TEMP/flux-bin/flux" ./cmd/flux
- name: Setup flux with plugin
id: setup
uses: ./action
with:
version: 0.0.0-pr
bindir: ${{ runner.temp }}/flux-bin
plugins: |
mirror@sha256:91a1e04c2015ee66b1633e362cdb6d4550891b91bf3391a83414b9e9912e53f1
- name: Assert plugin installation
shell: bash
env:
ACTION_PLUGIN_DIR: ${{ steps.setup.outputs['plugin-dir'] }}
run: |
set -euo pipefail
test -n "${FLUXCD_PLUGINS:-}"
test "$ACTION_PLUGIN_DIR" = "$FLUXCD_PLUGINS"
test -x "$FLUXCD_PLUGINS/flux-mirror"
flux plugin list
flux mirror --help

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.11.0 uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
secrets: secrets:
github-token: ${{ secrets.BOT_GITHUB_TOKEN }} github-token: ${{ secrets.BOT_GITHUB_TOKEN }}

View file

@ -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.34.1, 1.35.2, 1.36.1] KUBERNETES_VERSION: [1.33.0, 1.34.1, 1.35.0]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
cache-dependency-path: | cache-dependency-path: |
@ -42,7 +42,7 @@ jobs:
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with: with:
version: v0.32.0 version: v0.30.0
cluster_name: ${{ steps.prep.outputs.CLUSTER }} cluster_name: ${{ steps.prep.outputs.CLUSTER }}
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64 node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
- name: Run e2e tests - name: Run e2e tests
@ -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.34.8, 1.35.5, 1.36.1 ] K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # 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@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0 uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: "k3s" kubernetes-distribution: "k3s"
@ -150,7 +150,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@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0 uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.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 +168,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, 4.21.0-okd ] OPENSHIFT_VERSION: [ 4.20.0-okd ]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
cache-dependency-path: | cache-dependency-path: |
@ -189,7 +189,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@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
- name: Build - name: Build
run: make build-dev run: make build-dev
- name: Create repository - name: Create repository
@ -199,7 +199,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@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0 uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: "openshift" kubernetes-distribution: "openshift"
@ -240,7 +240,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@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0 uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
continue-on-error: true continue-on-error: true
with: with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }} api-token: ${{ secrets.REPLICATED_API_TOKEN }}

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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.26.x go-version: 1.26.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@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
- name: Setup Flux CLI - name: Setup Flux CLI
run: make build run: make build
working-directory: ./ working-directory: ./
@ -48,7 +48,7 @@ jobs:
env: env:
SOPS_VER: 3.7.1 SOPS_VER: 3.7.1
- name: Authenticate to Azure - name: Authenticate to Azure
uses: Azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v1.4.6 uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v1.4.6
with: with:
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}' creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
- name: Set dynamic variables in .env - name: Set dynamic variables in .env

View file

@ -17,9 +17,9 @@ 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.26.x go-version: 1.26.x
cache-dependency-path: | cache-dependency-path: |
@ -28,16 +28,16 @@ jobs:
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with: with:
version: v0.32.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.36.1-amd64 node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
kubectl_version: v1.36.0 kubectl_version: v1.33.0
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
- name: Setup yq - name: Setup yq
uses: fluxcd/pkg/actions/yq@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/yq@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
- name: Build - name: Build
run: make build-dev run: make build-dev
- name: Set outputs - name: Set outputs

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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.26.x go-version: 1.26.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@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
- 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@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log into us-central1-docker.pkg.dev - name: Log into us-central1-docker.pkg.dev
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
registry: us-central1-docker.pkg.dev registry: us-central1-docker.pkg.dev
username: oauth2accesstoken username: oauth2accesstoken

View file

@ -18,14 +18,14 @@ jobs:
labels: ubuntu-latest-16-cores labels: ubuntu-latest-16-cores
services: services:
registry: registry:
image: registry:3 image: registry:2
ports: ports:
- 5000:5000 - 5000:5000
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.26.x go-version: 1.26.x
cache-dependency-path: | cache-dependency-path: |
@ -34,19 +34,19 @@ jobs:
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
with: with:
version: v0.32.0 version: v0.30.0
cluster_name: kind cluster_name: kind
wait: 5s wait: 5s
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.34.1-amd64 node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
kubectl_version: v1.34.0 kubectl_version: v1.33.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.32.0/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@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
- name: Run tests - name: Run tests
run: make test run: make test
- name: Run e2e tests - name: Run e2e tests

View file

@ -19,7 +19,7 @@ jobs:
actions: read actions: read
contents: read contents: read
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -22,35 +22,35 @@ jobs:
packages: write # needed for ghcr access packages: write # needed for ghcr access
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Unshallow - name: Unshallow
run: git fetch --prune --unshallow run: git fetch --prune --unshallow
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.26.x go-version: 1.26.x
cache: false cache: false
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Setup Syft - name: Setup Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
- name: Setup Cosign - name: Setup Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with: with:
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3 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@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -63,7 +63,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@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/crdjsonschema@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
with: with:
crd: all-crds.yaml crd: all-crds.yaml
output: schemas output: schemas
@ -72,7 +72,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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
with: with:
version: latest version: latest
args: release --skip=validate args: release --skip=validate
@ -103,9 +103,9 @@ jobs:
id-token: write id-token: write
packages: write packages: write
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
- name: Setup Flux CLI - name: Setup Flux CLI
uses: ./action/ uses: ./action/
with: with:
@ -116,13 +116,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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -150,7 +150,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@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with: with:
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3 cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
- name: Sign manifests - name: Sign manifests

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.11.0 uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0
secrets: secrets:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
fossa-token: ${{ secrets.FOSSA_TOKEN }} fossa-token: ${{ secrets.FOSSA_TOKEN }}

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.11.0 uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0
secrets: secrets:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -16,9 +16,9 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with: with:
go-version: 1.26.x go-version: 1.26.x
cache-dependency-path: | cache-dependency-path: |
@ -96,7 +96,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
commit-message: | commit-message: |

View file

@ -3,11 +3,8 @@ name: upgrade-fluxcd-pkg
on: on:
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
upgrade-fluxcd-pkg: upgrade-fluxcd-pkg:
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.11.0 uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
secrets: secrets:
github-token: ${{ secrets.BOT_GITHUB_TOKEN }} github-token: ${{ secrets.BOT_GITHUB_TOKEN }}

151
AGENTS.md
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.

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-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets)
- [source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns
- [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.26
* kubectl >= 1.33
* kustomize >= 5.0
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
```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).

View file

@ -3,7 +3,7 @@ FROM alpine:3.23 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.36.1 ARG KUBECTL_VER=1.35.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

View file

@ -2,9 +2,8 @@ VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | head -n
DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s) DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s)
EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done
TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig
# Architecture to use envtest with; defaults to the host architecture. # Architecture to use envtest with
LOCALARCH ?= $(shell go env GOARCH) ENVTEST_ARCH ?= amd64
ENVTEST_ARCH ?= $(LOCALARCH)
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN)) ifeq (,$(shell go env GOBIN))

View file

@ -12,26 +12,6 @@ steps:
run: flux version --client run: flux version --client
``` ```
To install Flux plugins alongside the CLI:
```yaml
steps:
- name: Setup Flux CLI
uses: fluxcd/flux2/action@main
with:
plugins: |
mirror@0.0.1
- name: Run Flux plugin
run: flux mirror --help
```
Installing plugins requires a Flux version with plugin support (v2.9.0 or later).
The `plugins` input accepts one plugin per line in `<name>`,
`<name>@<version>`, or `<name>@<digest>` format. Entries without a version or
digest install the latest version from the catalog. The `plugin-dir` input is
only used when `plugins` is set; when plugins are installed, the action exports
`FLUXCD_PLUGINS` for subsequent steps.
The Flux GitHub Action can be used to automate various tasks in CI, such as: The Flux GitHub Action can be used to automate various tasks in CI, such as:
- [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates) - [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates)

View file

@ -12,36 +12,17 @@ inputs:
description: "arch can be amd64, arm64 or arm" description: "arch can be amd64, arm64 or arm"
required: false required: false
deprecationMessage: "No longer required, action will now detect runner arch." deprecationMessage: "No longer required, action will now detect runner arch."
plugins:
description: >
Plugins to install alongside Flux. One per line: `<name>`, `<name>@<version>` or
`<name>@<digest>`. Entries without a version or digest install the latest
version from the catalog. Example: `mirror@0.0.1`
required: false
plugin-dir:
description: >
Directory where requested plugins are installed. This input is only used when
`plugins` is set. When plugins are installed, the action exports
`FLUXCD_PLUGINS` for subsequent steps. Defaults to a `plugins` directory
alongside the Flux binary in $RUNNER_TOOL_CACHE.
required: false
bindir: bindir:
description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE." description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
required: false required: false
token: token:
description: "Token used to authenticate against the GitHub.com API." description: "Token used to authenticate against the GitHub.com API."
required: false required: false
outputs:
plugin-dir:
description: >
Directory where the plugins were installed by the action.
value: ${{ steps.set-outputs.outputs.plugin-dir }}
runs: runs:
using: composite using: composite
steps: steps:
- name: "Download the binary to the runner's cache dir" - name: "Download the binary to the runner's cache dir"
shell: bash shell: bash
id: setup-flux-bin
env: env:
VERSION: "${{ inputs.version }}" VERSION: "${{ inputs.version }}"
FLUX_TOOL_DIR: "${{ inputs.bindir }}" FLUX_TOOL_DIR: "${{ inputs.bindir }}"
@ -61,13 +42,11 @@ runs:
if [[ $VERSION = v* ]]; then if [[ $VERSION = v* ]]; then
VERSION="${VERSION:1}" VERSION="${VERSION:1}"
fi fi
echo "installed-flux-version=$VERSION" >> "$GITHUB_OUTPUT"
OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]') OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')
if [[ "$OS" == "macos" ]]; then if [[ "$OS" == "macos" ]]; then
OS="darwin" OS="darwin"
fi fi
echo "os=$OS" >> "$GITHUB_OUTPUT"
ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]') ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')
if [[ "$ARCH" == "x64" ]]; then if [[ "$ARCH" == "x64" ]]; then
@ -75,7 +54,6 @@ runs:
elif [[ "$ARCH" == "x86" ]]; then elif [[ "$ARCH" == "x86" ]]; then
ARCH="386" ARCH="386"
fi fi
echo "arch=$ARCH" >> "$GITHUB_OUTPUT"
FLUX_EXEC_FILE="flux" FLUX_EXEC_FILE="flux"
if [[ "$OS" == "windows" ]]; then if [[ "$OS" == "windows" ]]; then
@ -85,7 +63,7 @@ runs:
if [[ -z "$FLUX_TOOL_DIR" ]]; then if [[ -z "$FLUX_TOOL_DIR" ]]; then
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}" FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
fi fi
if [[ ! -x "$FLUX_TOOL_DIR/$FLUX_EXEC_FILE" ]]; then if [[ ! -x "$FLUX_TOOL_DIR/FLUX_EXEC_FILE" ]]; then
DL_DIR="$(mktemp -dt flux2-XXXXXX)" DL_DIR="$(mktemp -dt flux2-XXXXXX)"
trap 'rm -rf $DL_DIR' EXIT trap 'rm -rf $DL_DIR' EXIT
@ -101,7 +79,7 @@ runs:
MAX_RETRIES=5 MAX_RETRIES=5
RETRY_DELAY=5 RETRY_DELAY=5
for i in $(seq 1 $MAX_RETRIES); do for i in $(seq 1 $MAX_RETRIES); do
echo "Downloading flux binary (attempt $i/$MAX_RETRIES)" echo "Downloading flux binary (attempt $i/$MAX_RETRIES)"
if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then
@ -115,7 +93,7 @@ runs:
exit 1 exit 1
fi fi
done done
for i in $(seq 1 $MAX_RETRIES); do for i in $(seq 1 $MAX_RETRIES); do
echo "Downloading checksums file (attempt $i/$MAX_RETRIES)" echo "Downloading checksums file (attempt $i/$MAX_RETRIES)"
if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then
@ -129,7 +107,7 @@ runs:
exit 1 exit 1
fi fi
done done
echo "Verifying checksum" echo "Verifying checksum"
sum="" sum=""
if command -v openssl > /dev/null; then if command -v openssl > /dev/null; then
@ -151,7 +129,7 @@ runs:
echo "Installing flux to ${FLUX_TOOL_DIR}" echo "Installing flux to ${FLUX_TOOL_DIR}"
mkdir -p "$FLUX_TOOL_DIR" mkdir -p "$FLUX_TOOL_DIR"
if [[ "$OS" == "windows" ]]; then if [[ "$OS" == "windows" ]]; then
unzip "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_EXEC_FILE" -d "$FLUX_TOOL_DIR" unzip "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_EXEC_FILE" -d "$FLUX_TOOL_DIR"
else else
@ -168,63 +146,3 @@ runs:
shell: bash shell: bash
run: | run: |
flux -v flux -v
- name: "Plugin support gate"
id: plugin-support-gate
shell: bash
if: inputs.plugins != ''
run: |
flux_version="$(flux -v 2>/dev/null | awk '{print $3}')"
if flux plugin --help >/dev/null 2>&1; then
echo "plugin-support=yes" >> "$GITHUB_OUTPUT"
exit 0
else
echo "plugin-support=no" >> "$GITHUB_OUTPUT"
msg="Installed Flux version ${flux_version:-unknown} does not support plugins; need >= 2.9.0. Requested plugins cannot be installed."
echo "::error title=Unsupported Flux version::$msg"
echo "> [!CAUTION]" >> $GITHUB_STEP_SUMMARY
echo "> $msg" >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: "Setup Plugins Dir"
shell: bash
if: steps.plugin-support-gate.outputs.plugin-support == 'yes'
env:
FLUXCD_PLUGINS: ${{ inputs.plugin-dir }}
VERSION: ${{ steps.setup-flux-bin.outputs.installed-flux-version }}
OS: ${{ steps.setup-flux-bin.outputs.os }}
ARCH: ${{ steps.setup-flux-bin.outputs.arch }}
run: |
# Use a default plugin directory when plugin-dir was not provided.
if [[ -z "$FLUXCD_PLUGINS" ]]; then
FLUXCD_PLUGINS="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}/plugins/"
fi
# Export it so subsequent steps can discover the installed plugins.
echo "FLUXCD_PLUGINS=$FLUXCD_PLUGINS" >> "$GITHUB_ENV"
# Create the directory if it does not exist.
mkdir -p "$FLUXCD_PLUGINS"
- name: "Install Plugins"
shell: bash
id: install-plugins
if: steps.plugin-support-gate.outputs.plugin-support == 'yes'
env:
PLUGINS: ${{ inputs.plugins }}
run: |
echo "$PLUGINS" | while read -r PLUGIN; do
trimmed="${PLUGIN//[[:space:]]/}"
if [[ -n "$trimmed" ]]; then
echo Installing: "$PLUGIN"
flux plugin install "$PLUGIN"
fi
done
- name: "Set Output"
id: set-outputs
if: steps.plugin-support-gate.outputs.plugin-support == 'yes'
shell: bash
run: |
echo "plugin-dir=$FLUXCD_PLUGINS" >> "$GITHUB_OUTPUT"

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"
) )
@ -50,7 +47,6 @@ type bootstrapFlags struct {
branch string branch string
recurseSubmodules bool recurseSubmodules bool
sparseCheckout []string
manifestsPath string manifestsPath string
defaultComponents []string defaultComponents []string
@ -83,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
@ -118,8 +109,6 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false, bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false,
"when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces") "when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces")
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.sparseCheckout, "sparse-checkout", nil,
"list of directories to be included in the GitRepository sparse checkout, the configured --path must be one of them, accepts comma-separated values")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
@ -150,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")
@ -212,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
} }
@ -256,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 {

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"
@ -254,7 +253,6 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: bServerArgs.path.ToSlash(), TargetPath: bServerArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
SparseCheckout: bootstrapArgs.sparseCheckout,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
@ -289,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 {

View file

@ -28,12 +28,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/auth/aws"
authutils "github.com/fluxcd/pkg/auth/utils"
"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"
@ -66,12 +62,9 @@ command will perform an upgrade if needed.`,
# Run bootstrap for a Git repository with a private key and password # Run bootstrap for a Git repository with a private key and password
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --password=<password> --path=clusters/my-cluster flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --password=<password> --path=clusters/my-cluster
# Run bootstrap for a Git repository on AWS CodeCommit using SSH # Run bootstrap for a Git repository on AWS CodeCommit
flux bootstrap git --url=ssh://<SSH-Key-ID>@git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --private-key-file=<path/to/private.key> --password=<SSH-passphrase> --path=clusters/my-cluster flux bootstrap git --url=ssh://<SSH-Key-ID>@git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --private-key-file=<path/to/private.key> --password=<SSH-passphrase> --path=clusters/my-cluster
# Run bootstrap for a Git repository on AWS CodeCommit using HTTPS (requires AWS IAM credentials)
flux bootstrap git --url=https://git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --path=clusters/my-cluster
# Run bootstrap for a Git repository on Azure Devops # Run bootstrap for a Git repository on Azure Devops
flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --private-key-file=<path/to/rsa-sha2-private.key> --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --private-key-file=<path/to/rsa-sha2-private.key> --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster
@ -116,7 +109,6 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
bootstrapArgs.tokenAuth = true bootstrapArgs.tokenAuth = true
} }
var gitProvider string
gitPassword := os.Getenv(gitPasswordEnvVar) gitPassword := os.Getenv(gitPasswordEnvVar)
if gitPassword != "" && gitArgs.password == "" { if gitPassword != "" && gitArgs.password == "" {
gitArgs.password = gitPassword gitArgs.password = gitPassword
@ -139,12 +131,8 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") { if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") {
// https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control.html if repositoryURL.Scheme == string(git.SSH) {
if repositoryURL.Scheme == string(git.SSH) { // IAM user + SSH
if repositoryURL.User == nil { if repositoryURL.User == nil {
return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be specified in the url") return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be specified in the url")
} }
@ -154,19 +142,15 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
if bootstrapArgs.privateKeyFile == "" { if bootstrapArgs.privateKeyFile == "" {
return fmt.Errorf("private key file is required for bootstrapping against AWS CodeCommit using ssh") return fmt.Errorf("private key file is required for bootstrapping against AWS CodeCommit using ssh")
} }
} else if repositoryURL.Scheme == string(git.HTTPS) && !bootstrapArgs.tokenAuth { // IAM role + HTTPS
creds, err := authutils.GetGitCredentials(ctx, "aws", auth.WithGitURL(*repositoryURL))
if err != nil {
return fmt.Errorf("failed to get AWS CodeCommit IAM git credentials: %w", err)
}
gitArgs.username = creds.Username
gitArgs.password = creds.Password
bootstrapArgs.tokenAuth = true
gitProvider = aws.ProviderName
} }
if repositoryURL.Scheme == string(git.HTTPS) && !bootstrapArgs.tokenAuth {
return fmt.Errorf("--token-auth=true must be specified for using an HTTPS AWS CodeCommit url")
}
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
@ -312,10 +296,6 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: gitArgs.path.ToSlash(), TargetPath: gitArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
SparseCheckout: bootstrapArgs.sparseCheckout,
}
if gitProvider != "" {
syncOpts.Provider = gitProvider
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
@ -335,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 {

View file

@ -107,11 +107,6 @@ func init() {
} }
func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
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")
}
gtToken := os.Getenv(gtTokenEnvVar) gtToken := os.Getenv(gtTokenEnvVar)
if gtToken == "" { if gtToken == "" {
var err error var err error
@ -237,7 +232,6 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: giteaArgs.path.ToSlash(), TargetPath: giteaArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
SparseCheckout: bootstrapArgs.sparseCheckout,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
@ -258,7 +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.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
} }
@ -272,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 {

View file

@ -107,11 +107,6 @@ func init() {
} }
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
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")
}
ghToken := os.Getenv(ghTokenEnvVar) ghToken := os.Getenv(ghTokenEnvVar)
if ghToken == "" { if ghToken == "" {
var err error var err error
@ -244,7 +239,6 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: githubArgs.path.ToSlash(), TargetPath: githubArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
SparseCheckout: bootstrapArgs.sparseCheckout,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
@ -265,7 +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.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
} }
@ -279,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 {

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"
@ -288,7 +287,6 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: gitlabArgs.path.ToSlash(), TargetPath: gitlabArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
SparseCheckout: bootstrapArgs.sparseCheckout,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
@ -323,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 {

View file

@ -1,208 +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) {
savedDefaultComponents := bootstrapArgs.defaultComponents
savedGpgRing := bootstrapArgs.gpgKeyRingPath
savedGpgPass := bootstrapArgs.gpgPassphrase
savedSshKey := bootstrapArgs.sshSigningKeyFile
savedSshPass := bootstrapArgs.sshSigningPassword
savedSshPassp := bootstrapArgs.sshSigningPassphrase
savedPrivKey := bootstrapArgs.privateKeyFile
savedReuse := bootstrapArgs.sshSigningReusePrivateKey
defer func() {
bootstrapArgs.defaultComponents = savedDefaultComponents
bootstrapArgs.gpgKeyRingPath = savedGpgRing
bootstrapArgs.gpgPassphrase = savedGpgPass
bootstrapArgs.sshSigningKeyFile = savedSshKey
bootstrapArgs.sshSigningPassword = savedSshPass
bootstrapArgs.sshSigningPassphrase = savedSshPassp
bootstrapArgs.privateKeyFile = savedPrivKey
bootstrapArgs.sshSigningReusePrivateKey = savedReuse
}()
// The e2e TestMain calls resetCmdArgs which clears the
// cobra-populated default components, so seed them here to
// satisfy the requiredComponents pre-check in bootstrapValidate.
bootstrapArgs.defaultComponents = bootstrapArgs.requiredComponents
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)
}
})
}
}
// Providers that generate the SSH transport key in-process (github, gitea)
// must reject --ssh-signing-reuse-private-key with their own, provider-
// specific error before bootstrapValidate runs — otherwise the generic
// "--ssh-signing-reuse-private-key requires --private-key-file" error
// shadows the fact that the flag is fundamentally unsupported there.
func TestBootstrapProviderRejectsReuseBeforeValidate(t *testing.T) {
tests := []struct {
name string
runE func() error
wantErr string
}{
{
name: "github rejects reuse with provider-specific error",
runE: func() error { return bootstrapGitHubCmdRun(nil, nil) },
wantErr: "not supported by 'bootstrap github'",
},
{
name: "gitea rejects reuse with provider-specific error",
runE: func() error { return bootstrapGiteaCmdRun(nil, nil) },
wantErr: "not supported by 'bootstrap gitea'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
savedReuse := bootstrapArgs.sshSigningReusePrivateKey
savedPrivKey := bootstrapArgs.privateKeyFile
defer func() {
bootstrapArgs.sshSigningReusePrivateKey = savedReuse
bootstrapArgs.privateKeyFile = savedPrivKey
}()
// Reuse flag set, no --private-key-file: bootstrapValidate
// would otherwise return "requires --private-key-file".
bootstrapArgs.sshSigningReusePrivateKey = true
bootstrapArgs.privateKeyFile = ""
err := tt.runE()
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)
}
})
}
}

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 {

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"))
}

View file

@ -72,7 +72,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 +85,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", false,
"Use in-memory filesystem during build.")
buildCmd.AddCommand(buildKsCmd) buildCmd.AddCommand(buildKsCmd)
} }
@ -133,7 +130,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 +140,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),
) )
} }

View file

@ -23,8 +23,6 @@ import (
"bytes" "bytes"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"testing" "testing"
"text/template" "text/template"
) )
@ -54,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 (in-memory)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=true",
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",
@ -78,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 (in-memory)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=true",
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 (in-memory)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=true",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{
@ -165,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 (in-memory)",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=true",
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",
@ -201,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 (in-memory)",
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=true",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive in dry-run mode (in-memory)",
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=true --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{
@ -257,104 +219,6 @@ spec:
} }
} }
func TestBuildKustomizationDefaultBuildsAbsolutePathOutsideCwd(t *testing.T) {
cwdDir, sourceDir, kustomizationFile := newOutsideCwdKustomization(t, "default", "")
restore := chdirForTest(t, cwdDir)
defer restore()
flag := buildKsCmd.Flags().Lookup("in-memory-build")
if flag == nil {
t.Fatal("missing in-memory-build flag")
}
defaultValue, err := strconv.ParseBool(flag.DefValue)
if err != nil {
t.Fatal(err)
}
buildKsArgs.inMemoryBuild = defaultValue
output, err := executeCommand("build kustomization app --path " + sourceDir +
" --kustomization-file " + kustomizationFile +
" --namespace default --dry-run")
if err != nil {
t.Fatalf("expected build to succeed with default backend, got: %v", err)
}
if !strings.Contains(output, "name: outside-cwd") {
t.Fatalf("expected rendered ConfigMap in output, got:\n%s", output)
}
}
func chdirForTest(t *testing.T, dir string) func() {
t.Helper()
orig, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
return func() {
if err := os.Chdir(orig); err != nil {
t.Fatal(err)
}
}
}
func newOutsideCwdKustomization(t *testing.T, namespace, targetNamespace string) (string, string, string) {
t.Helper()
parentDir := t.TempDir()
cwdDir := filepath.Join(parentDir, "cwd")
if err := os.MkdirAll(cwdDir, 0o755); err != nil {
t.Fatal(err)
}
sourceDir := filepath.Join(parentDir, "source")
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sourceDir, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
`), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sourceDir, "configmap.yaml"), []byte(`apiVersion: v1
kind: ConfigMap
metadata:
name: outside-cwd
`), 0o644); err != nil {
t.Fatal(err)
}
targetNamespaceField := ""
if targetNamespace != "" {
targetNamespaceField = "\n targetNamespace: " + targetNamespace
}
kustomizationFile := filepath.Join(parentDir, "app.yaml")
if err := os.WriteFile(kustomizationFile, []byte(`apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app
namespace: `+namespace+`
spec:
interval: 5m
path: ./source
prune: true`+targetNamespaceField+`
sourceRef:
kind: GitRepository
name: app
`), 0o644); err != nil {
t.Fatal(err)
}
return cwdDir, sourceDir, kustomizationFile
}
// TestBuildKustomizationPathNormalization verifies that absolute and complex // TestBuildKustomizationPathNormalization verifies that absolute and complex
// paths are normalized to prevent path concatenation bugs (issue #5673). // paths are normalized to prevent path concatenation bugs (issue #5673).
// Without normalization, paths could be duplicated like: /path/test/path/test/file // Without normalization, paths could be duplicated like: /path/test/path/test/file
@ -377,12 +241,6 @@ func TestBuildKustomizationPathNormalization(t *testing.T) {
resultFile: "./testdata/build-kustomization/podinfo-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build with absolute path (in-memory)",
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=true",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{ {
name: "build with complex relative path (parent dir)", name: "build with complex relative path (parent dir)",
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo", args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",

View file

@ -218,7 +218,7 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
} }
if len(helmReleaseArgs.dependsOn) > 0 { if len(helmReleaseArgs.dependsOn) > 0 {
ls := meta.MakeDependsOn(helmReleaseArgs.dependsOn) ls := utils.MakeDependsOn(helmReleaseArgs.dependsOn)
hrDependsOn := make([]helmv2.DependencyReference, 0, len(ls)) hrDependsOn := make([]helmv2.DependencyReference, 0, len(ls))
for _, d := range ls { for _, d := range ls {
hrDependsOn = append(hrDependsOn, helmv2.DependencyReference{ hrDependsOn = append(hrDependsOn, helmv2.DependencyReference{

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,13 +163,6 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
} }
} }
if imageUpdateArgs.signingKeySecret != "" {
update.Spec.GitSpec.Commit.SigningKey = &autov1.SigningKey{
SecretRef: meta.LocalObjectReference{Name: imageUpdateArgs.signingKeySecret},
Type: autov1.SigningKeyType(imageUpdateArgs.signingKeyType),
}
}
if createArgs.export { if createArgs.export {
return printExport(exportImageUpdate(&update)) return printExport(exportImageUpdate(&update))
} }

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)
})
}
}

View file

@ -172,7 +172,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
} }
if len(kustomizationArgs.dependsOn) > 0 { if len(kustomizationArgs.dependsOn) > 0 {
ls := meta.MakeDependsOn(kustomizationArgs.dependsOn) ls := utils.MakeDependsOn(kustomizationArgs.dependsOn)
ksDependsOn := make([]kustomizev1.DependencyReference, 0, len(ls)) ksDependsOn := make([]kustomizev1.DependencyReference, 0, len(ls))
for _, d := range ls { for _, d := range ls {
ksDependsOn = append(ksDependsOn, kustomizev1.DependencyReference{ ksDependsOn = append(ksDependsOn, kustomizev1.DependencyReference{

View file

@ -77,18 +77,16 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("secret ref is required") return fmt.Errorf("secret ref is required")
} }
resources := []notificationv1.ReceiverResource{} resources := []notificationv1.CrossNamespaceObjectReference{}
for _, resource := range receiverArgs.resources { for _, resource := range receiverArgs.resources {
kind, name := utils.ParseObjectKindName(resource) kind, name := utils.ParseObjectKindName(resource)
if kind == "" { if kind == "" {
return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", resource) return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", resource)
} }
resources = append(resources, notificationv1.ReceiverResource{ resources = append(resources, notificationv1.CrossNamespaceObjectReference{
CrossNamespaceObjectReference: notificationv1.CrossNamespaceObjectReference{ Kind: kind,
Kind: kind, Name: name,
Name: name,
},
}) })
} }
@ -115,7 +113,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
Type: receiverArgs.receiverType.String(), Type: receiverArgs.receiverType.String(),
Events: receiverArgs.events, Events: receiverArgs.events,
Resources: resources, Resources: resources,
SecretRef: &meta.LocalObjectReference{ SecretRef: meta.LocalObjectReference{
Name: receiverArgs.secretRef, Name: receiverArgs.secretRef,
}, },
Suspend: false, Suspend: false,

View file

@ -130,7 +130,7 @@ func createSourceChartCmdRun(cmd *cobra.Command, args []string) error {
} }
if provider := sourceChartArgs.verifyProvider.String(); provider != "" { if provider := sourceChartArgs.verifyProvider.String(); provider != "" {
helmChart.Spec.Verify = &sourcev1.HelmChartVerification{ helmChart.Spec.Verify = &sourcev1.OCIRepositoryVerification{
Provider: provider, Provider: provider,
} }
if secretName := sourceChartArgs.verifySecretRef; secretName != "" { if secretName := sourceChartArgs.verifySecretRef; secretName != "" {

View file

@ -124,12 +124,6 @@ For private Git repositories, the basic authentication credentials are stored in
--username=username \ --username=username \
--password=password --password=password
# Create a source for a Git repository using AWS CodeCommit with IAM credentials
flux create source git podinfo \
--url=https://git-codecommit.<region>.amazonaws.com/v1/repos/podinfo \
--branch=master \
--provider=aws
# Create a source for a Git repository using azure provider # Create a source for a Git repository using azure provider
flux create source git podinfo \ flux create source git podinfo \
--url=https://dev.azure.com/foo/bar/_git/podinfo \ --url=https://dev.azure.com/foo/bar/_git/podinfo \

View file

@ -152,7 +152,7 @@ func TestCreateSourceGitExport(t *testing.T) {
{ {
name: "source with invalid provider", name: "source with invalid provider",
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider dummy --branch=test --interval=1m0s --export", args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider dummy --branch=test --interval=1m0s --export",
assert: assertError("invalid argument \"dummy\" for \"--provider\" flag: source Git provider 'dummy' is not supported, must be one of: generic|github|aws|azure"), assert: assertError("invalid argument \"dummy\" for \"--provider\" flag: source Git provider 'dummy' is not supported, must be one of: generic|azure|github"),
}, },
{ {
name: "source with empty provider", name: "source with empty provider",

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
} }

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",

View file

@ -53,12 +53,6 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
--verify-provider=cosign \ --verify-provider=cosign \
--verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \ --verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \
--verify-issuer="^https://token.actions.githubusercontent.com$" --verify-issuer="^https://token.actions.githubusercontent.com$"
# Create an OCIRepository for a Helm chart layer
flux create source oci valkey-cluster \
--url=oci://example.com/charts/valkey \
--tag=0.11.6 \
--layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:copy
`, `,
RunE: createSourceOCIRepositoryCmdRun, RunE: createSourceOCIRepositoryCmdRun,
} }
@ -79,7 +73,6 @@ type sourceOCIRepositoryFlags struct {
ignorePaths []string ignorePaths []string
provider flags.SourceOCIProvider provider flags.SourceOCIProvider
insecure bool insecure bool
layerSelector string
} }
var sourceOCIRepositoryArgs = newSourceOCIFlags() var sourceOCIRepositoryArgs = newSourceOCIFlags()
@ -106,7 +99,6 @@ func init() {
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP") createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.layerSelector, "layer-selector", "", "the OCI artifact layer selector in the format '<media-type>:<operation>'")
createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) createSourceCmd.AddCommand(createSourceOCIRepositoryCmd)
} }
@ -122,11 +114,6 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("--tag, --tag-semver or --digest is required") return fmt.Errorf("--tag, --tag-semver or --digest is required")
} }
layerSelector, err := parseLayerSelector(sourceOCIRepositoryArgs.layerSelector)
if err != nil {
return err
}
sourceLabels, err := parseLabels() sourceLabels, err := parseLabels()
if err != nil { if err != nil {
return err return err
@ -165,7 +152,6 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
if tag := sourceOCIRepositoryArgs.tag; tag != "" { if tag := sourceOCIRepositoryArgs.tag; tag != "" {
repository.Spec.Reference.Tag = tag repository.Spec.Reference.Tag = tag
} }
repository.Spec.LayerSelector = layerSelector
if createSourceArgs.fetchTimeout > 0 { if createSourceArgs.fetchTimeout > 0 {
repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
@ -248,28 +234,6 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func parseLayerSelector(selector string) (*sourcev1.OCILayerSelector, error) {
if selector == "" {
return nil, nil
}
mediaType, operation, found := strings.Cut(selector, ":")
if !found || mediaType == "" || operation == "" {
return nil, fmt.Errorf("invalid --layer-selector %q: must be in the format '<media-type>:<operation>'", selector)
}
switch operation {
case sourcev1.OCILayerExtract, sourcev1.OCILayerCopy:
default:
return nil, fmt.Errorf("invalid --layer-selector %q: operation must be %q or %q", selector, sourcev1.OCILayerExtract, sourcev1.OCILayerCopy)
}
return &sourcev1.OCILayerSelector{
MediaType: mediaType,
Operation: operation,
}, nil
}
func upsertOCIRepository(ctx context.Context, kubeClient client.Client, func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) { ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{ namespacedName := types.NamespacedName{

View file

@ -81,21 +81,6 @@ func TestCreateSourceOCI(t *testing.T) {
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"), assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"),
}, },
{
name: "export manifest with layer selector",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:copy --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_layer_selector.golden"),
},
{
name: "invalid layer selector operation",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:move --export",
assertFunc: assertError("invalid --layer-selector \"application/vnd.cncf.helm.chart.content.v1.tar+gzip:move\": operation must be \"extract\" or \"copy\""),
},
{
name: "invalid layer selector format",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip --export",
assertFunc: assertError("invalid --layer-selector \"application/vnd.cncf.helm.chart.content.v1.tar+gzip\": must be in the format '<media-type>:<operation>'"),
},
} }
for _, tt := range tests { for _, tt := range tests {

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", false,
"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),
) )
} }

View file

@ -22,7 +22,6 @@ package main
import ( import (
"context" "context"
"os" "os"
"strconv"
"strings" "strings"
"testing" "testing"
@ -49,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",
@ -97,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",
@ -139,153 +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)
})
}
}
func TestDiffKustomizationDefaultBuildsAbsolutePathOutsideCwd(t *testing.T) {
fluxNS := allocateNamespace("flux-system")
targetNS := allocateNamespace("target")
setupTestNamespace(fluxNS, t)
setupTestNamespace(targetNS, t)
cwdDir, sourceDir, kustomizationFile := newOutsideCwdKustomization(t, fluxNS, targetNS)
restore := chdirForTest(t, cwdDir)
defer restore()
flag := diffKsCmd.Flags().Lookup("in-memory-build")
if flag == nil {
t.Fatal("missing in-memory-build flag")
}
defaultValue, err := strconv.ParseBool(flag.DefValue)
if err != nil {
t.Fatal(err)
}
diffKsArgs.inMemoryBuild = defaultValue
output, err := executeCommand("diff kustomization app --path " + sourceDir +
" --kustomization-file " + kustomizationFile +
" --ignore-not-found --progress-bar=false -n " + fluxNS)
if isChangeError(err) {
err = nil
}
if err != nil {
t.Fatalf("expected diff to build with default backend, got: %v", err)
}
if !strings.Contains(output, "ConfigMap/"+targetNS+"/outside-cwd created") {
t.Fatalf("expected created ConfigMap in diff output, got:\n%s", output)
}
}
// 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 {
@ -306,36 +158,3 @@ func createObjectFromFile(objectFile string, templateValues map[string]string, t
return clientObjects return clientObjects
} }
// TestDiffKustomizationDriftIgnoreRules tests `flux diff ks` with drift ignore
// rules. A service with a drifted port is pre-applied to the cluster, and the
// kustomization specifies driftIgnoreRules that ignore /spec/ports on Services.
// The diff should not show the service as drifted.
func TestDiffKustomizationDriftIgnoreRules(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupTestNamespace(tmpl["fluxns"], t)
b, _ := build.NewBuilder("podinfo", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
resourceManager, err := b.Manager()
if err != nil {
t.Fatal(err)
}
// Pre-apply the drifted service (port 9899 instead of 9898) without Flux labels.
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/drifted-service-no-labels.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
t.Fatal(err)
}
cmd := cmdTestCase{
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-drift-ignore.yaml " +
"--ignore-not-found" +
" -n " + tmpl["fluxns"],
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drift-ignore.golden"),
}
cmd.runTestCmd(t)
testEnv.DeleteObjectFile("./testdata/diff-kustomization/drifted-service-no-labels.yaml", tmpl, t)
}

View file

@ -78,7 +78,7 @@ func init() {
getCmd.PersistentFlags().BoolVarP(&getArgs.noHeader, "no-header", "", false, "skip the header when printing the results") getCmd.PersistentFlags().BoolVarP(&getArgs.noHeader, "no-header", "", false, "skip the header when printing the results")
getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.") getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.")
getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "", getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "",
"specify the status condition name and the desired state to filter the get result, e.g. ready=false or ready!=true") "specify the status condition name and the desired state to filter the get result, e.g. ready=false")
getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "", getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "",
"filter objects by label selector") "filter objects by label selector")
rootCmd.AddCommand(getCmd) rootCmd.AddCommand(getCmd)
@ -114,11 +114,6 @@ func statusMatches(conditionType, conditionStatus string, conditions []metav1.Co
return false return false
} }
func readyStatusMatches(conditionType, conditionStatus string) bool {
return strings.EqualFold(conditionType, meta.ReadyCondition) &&
strings.EqualFold(conditionStatus, string(metav1.ConditionTrue))
}
func nameColumns(item named, includeNamespace bool, includeKind bool) []string { func nameColumns(item named, includeNamespace bool, includeKind bool) []string {
name := item.GetName() name := item.GetName()
if includeKind { if includeKind {
@ -212,9 +207,6 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
if getAll && len(rows) == 0 {
return nil
}
err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows) err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
if err != nil { if err != nil {
@ -236,31 +228,20 @@ func namespaceNameOrAny(allNamespaces bool, namespaceName string) string {
} }
func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) { func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) {
filter := func(i int) bool { return true } noFilter := true
var conditionType, conditionStatus string var conditionType, conditionStatus string
if getArgs.statusSelector != "" { if getArgs.statusSelector != "" {
// Support both type=status (match) and type!=status (negated match). parts := strings.SplitN(getArgs.statusSelector, "=", 2)
// "!=" must be checked first since it also contains "=".
separator := "="
filter = func(i int) bool {
return list.statusSelectorMatches(i, conditionType, conditionStatus)
}
if strings.Contains(getArgs.statusSelector, "!=") {
separator = "!="
filter = func(i int) bool {
return !list.statusSelectorMatches(i, conditionType, conditionStatus)
}
}
parts := strings.SplitN(getArgs.statusSelector, separator, 2)
if len(parts) != 2 { if len(parts) != 2 {
return nil, fmt.Errorf("expected status selector in type=status or type!=status format, but found: %s", getArgs.statusSelector) return nil, fmt.Errorf("expected status selector in type=status format, but found: %s", getArgs.statusSelector)
} }
conditionType = parts[0] conditionType = parts[0]
conditionStatus = parts[1] conditionStatus = parts[1]
noFilter = false
} }
var rows [][]string var rows [][]string
for i := 0; i < list.len(); i++ { for i := 0; i < list.len(); i++ {
if filter(i) { if noFilter || list.statusSelectorMatches(i, conditionType, conditionStatus) {
row := list.summariseItem(i, getArgs.allNamespaces, getAll) row := list.summariseItem(i, getArgs.allNamespaces, getAll)
rows = append(rows, row) rows = append(rows, row)
} }

View file

@ -92,5 +92,5 @@ func (s alertListAdapter) headers(includeNamespace bool) []string {
} }
func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
return readyStatusMatches(conditionType, conditionStatus) return false
} }

View file

@ -88,5 +88,5 @@ func (s alertProviderListAdapter) headers(includeNamespace bool) []string {
} }
func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
return readyStatusMatches(conditionType, conditionStatus) return false
} }

View file

@ -38,7 +38,7 @@ var getArtifactGeneratorCmd = &cobra.Command{
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)), ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
get := getCommand{ get := getCommand{
apiType: artifactGeneratorType, apiType: receiverType,
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}}, list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
funcMap: make(typeMap), funcMap: make(typeMap),
} }

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...)
} }

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...)
} }

View file

@ -63,141 +63,6 @@ func Test_GetCmd(t *testing.T) {
} }
} }
func Test_GetCmdStatusSelector(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile("./testdata/get/status_objects.yaml", tmpl, t)
tests := []struct {
name string
args string
expected string
}{
{
name: "equal status selector matches one",
args: "--status-selector Ready=True",
expected: "testdata/get/get_status_ready_true.golden",
},
{
name: "equal status selector matches false",
args: "--status-selector Ready=False",
expected: "testdata/get/get_status_ready_false.golden",
},
{
name: "not-equal status selector matches all not-true",
args: "--status-selector Ready!=True",
expected: "testdata/get/get_status_ready_not_true.golden",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: "get sources git " + tt.args + " -n " + tmpl["fluxns"],
assert: assertGoldenTemplateFile(tt.expected, nil),
}
cmd.runTestCmd(t)
})
}
}
func Test_GetCmdStatusSelectorSyntheticReady(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile("./testdata/get/notification_objects.yaml", tmpl, t)
commands := []string{
"get alerts",
"get alert-providers",
"get all",
}
for _, command := range commands {
t.Run(command, func(t *testing.T) {
unfilteredOutput, err := executeCommand(command + " -n " + tmpl["fluxns"])
if err != nil {
t.Fatalf("%s failed: %v", command, err)
}
if unfilteredOutput == "" {
t.Fatalf("expected %s output for namespace with notification objects", command)
}
filteredOutput, err := executeCommand(command + " --status-selector Ready=True -n " + tmpl["fluxns"])
if err != nil {
t.Fatalf("%s with Ready=True status selector failed: %v", command, err)
}
if filteredOutput != unfilteredOutput {
t.Fatalf("expected Ready=True filtered output to match unfiltered output:\nfiltered:\n%s\nunfiltered:\n%s", filteredOutput, unfilteredOutput)
}
})
}
}
func Test_GetAllCmdStatusSelectorNoMatches(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile("./testdata/get/status_objects.yaml", tmpl, t)
cmd := cmdTestCase{
args: "get all --status-selector foo=bar -n " + tmpl["fluxns"],
assert: assertGoldenValue(""),
}
cmd.runTestCmd(t)
}
func Test_GetAllCmdStatusSelectorKustomizationOnlyMatches(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile("./testdata/get/kustomization_only.yaml", tmpl, t)
unfilteredOutput, err := executeCommand("get all -n " + tmpl["fluxns"])
if err != nil {
t.Fatalf("get all failed: %v", err)
}
if unfilteredOutput == "" {
t.Fatal("expected get all output for namespace with one Kustomization")
}
filteredOutput, err := executeCommand("get all --status-selector Ready=True -n " + tmpl["fluxns"])
if err != nil {
t.Fatalf("get all with matching status selector failed: %v", err)
}
if filteredOutput != unfilteredOutput {
t.Fatalf("expected filtered output to match unfiltered output:\nfiltered:\n%s\nunfiltered:\n%s", filteredOutput, unfilteredOutput)
}
}
func Test_GetAllCmdStatusSelectorKustomizationOnlyNoMatch(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile("./testdata/get/kustomization_only.yaml", tmpl, t)
emptyNamespace := allocateNamespace("empty")
setupTestNamespace(emptyNamespace, t)
emptyOutput, err := executeCommand("get all -n " + emptyNamespace)
if err != nil {
t.Fatalf("get all in empty namespace failed: %v", err)
}
filteredOutput, err := executeCommand("get all --status-selector Ready=False -n " + tmpl["fluxns"])
if err != nil {
t.Fatalf("get all with non-matching status selector failed: %v", err)
}
if filteredOutput != emptyOutput {
t.Fatalf("expected filtered output to match empty namespace output:\nfiltered:\n%s\nempty namespace:\n%s", filteredOutput, emptyOutput)
}
}
func Test_GetCmdErrors(t *testing.T) { func Test_GetCmdErrors(t *testing.T) {
tmpl := map[string]string{ tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"), "fluxns": allocateNamespace("flux-system"),
@ -219,16 +84,6 @@ func Test_GetCmdErrors(t *testing.T) {
args: "get helmrelease -n " + tmpl["fluxns"], args: "get helmrelease -n " + tmpl["fluxns"],
assert: assertError(fmt.Sprintf("no HelmRelease objects found in \"%s\" namespace", tmpl["fluxns"])), assert: assertError(fmt.Sprintf("no HelmRelease objects found in \"%s\" namespace", tmpl["fluxns"])),
}, },
{
name: "no artifact generators found in namespace",
args: "get artifact generators -n " + tmpl["fluxns"],
assert: assertError(fmt.Sprintf("no ArtifactGenerator objects found in \"%s\" namespace", tmpl["fluxns"])),
},
{
name: "malformed status selector",
args: "get sources git --status-selector Ready -n " + tmpl["fluxns"],
assert: assertError("expected status selector in type=status or type!=status format, but found: Ready"),
},
} }
for _, tt := range tests { for _, tt := range tests {

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
@ -199,8 +186,6 @@ func main() {
// 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")

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))
})
}
}

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 {

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
}

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
}

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)
}

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)
}

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)
}
}

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
}

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
}

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, "=")

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)
}
} }
} }

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
} }

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-----

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.

View file

@ -1 +0,0 @@
not a real ssh key

View file

@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: existing-config
namespace: default
data:
key: value

View file

@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./existing.yaml
- ./new.yaml

View file

@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: new-config
namespace: default
data:
key: value

View file

@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: new-ns
data:
key: value

View file

@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./namespace.yaml
- ./configmap.yaml

View file

@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: new-ns

View file

@ -1,4 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./namespace.yaml

View file

@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: new-ns

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

View file

@ -1,22 +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
interval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system

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

View file

@ -1 +0,0 @@
â–ş Namespace/new-ns created

View file

@ -1,9 +0,0 @@
â–ş ConfigMap/default/existing-config drifted
metadata
+ one map entry added:
labels:
kustomize.toolkit.fluxcd.io/name: configmaps
kustomize.toolkit.fluxcd.io/namespace:
â–ş ConfigMap/default/new-config created

View file

@ -1,14 +0,0 @@
â–ş Deployment/default/podinfo created
â–ş HorizontalPodAutoscaler/default/podinfo created
â–ş Service/default/podinfo drifted
metadata
+ one map entry added:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace:
â–ş Secret/default/docker-secret created
â–ş Secret/default/secret-basic-auth-stringdata created
â–ş Secret/default/podinfo-token-77t89m9b67 created
â–ş Secret/default/db-user-pass-bkbd782d2c created

View file

@ -1,18 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: podinfo
namespace: default
spec:
type: ClusterIP
selector:
app: podinfo
ports:
- name: http
port: 9899
protocol: TCP
targetPort: http
- port: 9999
targetPort: grpc
protocol: TCP
name: grpc

View file

@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: existing-config
namespace: default
data:
key: value

View file

@ -1,14 +0,0 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: configmaps
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: configmaps
targetNamespace: default

View file

@ -1,19 +0,0 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default
ignore:
- paths:
- "/spec/ports"
target:
kind: Service

View file

@ -1,14 +0,0 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default

View file

@ -1,13 +0,0 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: new-namespace-and-configmap
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: new-namespace-and-configmap

View file

@ -1,13 +0,0 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: new-namespace-only
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: new-namespace-only

View file

@ -10,10 +10,6 @@ spec:
author: author:
email: fluxcdbot@users.noreply.github.com email: fluxcdbot@users.noreply.github.com
name: fluxcdbot name: fluxcdbot
signingKey:
secretRef:
name: my-signing-key
type: ssh
interval: 1m0s interval: 1m0s
sourceRef: sourceRef:
kind: GitRepository kind: GitRepository

View file

@ -67,10 +67,6 @@ spec:
email: fluxcdbot@users.noreply.github.com email: fluxcdbot@users.noreply.github.com
name: fluxcdbot name: fluxcdbot
messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}' messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
signingKey:
secretRef:
name: my-signing-key
type: ssh
update: update:
path: ./clusters/my-cluster path: ./clusters/my-cluster
strategy: Setters strategy: Setters

View file

@ -1,2 +0,0 @@
NAME REVISION SUSPENDED READY MESSAGE
gr-failed False False failed to checkout and determine revision

View file

@ -1,3 +0,0 @@
NAME REVISION SUSPENDED READY MESSAGE
gr-failed False False failed to checkout and determine revision
gr-unknown False Unknown reconciliation in progress

View file

@ -1,2 +0,0 @@
NAME REVISION SUSPENDED READY MESSAGE
gr-ready main@sha1:696f056d False True Fetched revision: main@sha1:696f056d

View file

@ -1,26 +0,0 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .fluxns }}
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
interval: 5m
path: ./clusters/production
prune: true
sourceRef:
kind: GitRepository
name: podinfo
status:
lastAppliedRevision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f
conditions:
- lastTransitionTime: "2021-08-01T04:52:56Z"
message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f'
reason: ReconciliationSucceeded
status: "True"
type: Ready

View file

@ -1,31 +0,0 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .fluxns }}
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Provider
metadata:
name: slack
namespace: {{ .fluxns }}
spec:
address: https://hooks.slack.com/services/mock
channel: alerts
type: slack
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Alert
metadata:
name: flux-system
namespace: {{ .fluxns }}
spec:
eventSeverity: info
eventSources:
- kind: GitRepository
name: '*'
- kind: Kustomization
name: '*'
providerRef:
name: slack
summary: Slack notification

View file

@ -1,71 +0,0 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .fluxns }}
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: gr-failed
namespace: {{ .fluxns }}
spec:
ref:
branch: main
secretRef:
name: flux-system
url: ssh://git@github.com/example/repo
interval: 5m
status:
conditions:
- lastTransitionTime: "2021-07-20T00:48:16Z"
message: 'failed to checkout and determine revision'
reason: GitOperationFailed
status: "False"
type: Ready
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: gr-ready
namespace: {{ .fluxns }}
spec:
ref:
branch: main
secretRef:
name: flux-system
url: ssh://git@github.com/example/repo
interval: 5m
status:
artifact:
lastUpdateTime: "2021-08-01T04:28:42Z"
revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f
path: "example"
url: "example"
digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f
conditions:
- lastTransitionTime: "2021-07-20T00:48:16Z"
message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f'
reason: GitOperationSucceed
status: "True"
type: Ready
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: gr-unknown
namespace: {{ .fluxns }}
spec:
ref:
branch: main
secretRef:
name: flux-system
url: ssh://git@github.com/example/repo
interval: 5m
status:
conditions:
- lastTransitionTime: "2021-07-20T00:48:16Z"
message: 'reconciliation in progress'
reason: Progressing
status: "Unknown"
type: Ready

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