Compare commits
177 Commits
release/v2
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da88a8e040 | ||
|
|
c8b4c4c620 | ||
|
|
c031d0c215 | ||
|
|
4f5b2fcab9 | ||
|
|
df3878d36a | ||
|
|
4e78a9d7e0 | ||
|
|
c1238ec834 | ||
|
|
99a7d2d735 | ||
|
|
19ab6eeb30 | ||
|
|
00d918ecaa | ||
|
|
474efa09cf | ||
|
|
5256361d8c | ||
|
|
c0938d351f | ||
|
|
2cee1d795e | ||
|
|
9a4b93056b | ||
|
|
8be056324a | ||
|
|
e45e46211b | ||
|
|
aa608bb769 | ||
|
|
7d27a26665 | ||
|
|
e9bcccfede | ||
|
|
d349ffe37d | ||
|
|
ac7f72b62b | ||
|
|
968bebadf6 | ||
|
|
2bfdadd301 | ||
|
|
36686b945c | ||
|
|
4e52adc7f0 | ||
|
|
21ca8d4d17 | ||
|
|
3e198177da | ||
|
|
7ba6dacc5c | ||
|
|
c97bdd412f | ||
|
|
4eaf59113f | ||
|
|
082a706f7f | ||
|
|
8668902dd1 | ||
|
|
8bc3ba3e1c | ||
|
|
2fdbde7fde | ||
|
|
7d7f20da25 | ||
|
|
e5128ea97e | ||
|
|
4f2374178c | ||
|
|
125464ed72 | ||
|
|
69e2c6bc7d | ||
|
|
7c9810ea3b | ||
|
|
c601a212f6 | ||
|
|
02734f28ba | ||
|
|
3d4eec61fe | ||
|
|
8a777bdd0f | ||
|
|
e2af45aee4 | ||
|
|
befe53a722 | ||
|
|
241d703e7f | ||
|
|
c432d380dd | ||
|
|
457abed9f9 | ||
|
|
5fc8afcaaf | ||
|
|
7bf0bda689 | ||
|
|
d9f51d047d | ||
|
|
dc5631f12b | ||
|
|
3f9d5bdc3d | ||
|
|
64e18014c3 | ||
|
|
e9226713e8 | ||
|
|
6a5e644798 | ||
|
|
0b0be7c1b6 | ||
|
|
484346ffcc | ||
|
|
5b3acbfcb5 | ||
|
|
2288dd90d6 | ||
|
|
af05357a62 | ||
|
|
64808a0eac | ||
|
|
2ead4fb31c | ||
|
|
b60dfbe970 | ||
|
|
ee8bb8d8a0 | ||
|
|
5f3098477e | ||
|
|
4c79a76e94 | ||
|
|
1516761fc8 | ||
|
|
52b1c1152b | ||
|
|
ab4bbffa5b | ||
|
|
e7314e8926 | ||
|
|
2666eaf8fc | ||
|
|
8262f8099e | ||
|
|
cbc5c736f4 | ||
|
|
ac71dd88a3 | ||
|
|
c5e5dfb8ae | ||
|
|
6ae880501e | ||
|
|
fd547dfe42 | ||
|
|
436dc7920a | ||
|
|
7a8cf63623 | ||
|
|
a6aefab55b | ||
|
|
5e5ee73046 | ||
|
|
8362c88791 | ||
|
|
340a048e8b | ||
|
|
4b2fc84402 | ||
|
|
2673348c2f | ||
|
|
7132eb3435 | ||
|
|
473b02ce5c | ||
|
|
862d9ddb6d | ||
|
|
33b9345883 | ||
|
|
e169a97577 | ||
|
|
4eddf80724 | ||
|
|
99f182be06 | ||
|
|
cf785cebcc | ||
|
|
7ff4c32d16 | ||
|
|
75bf2d608f | ||
|
|
f950198f9d | ||
|
|
2a2201fe56 | ||
|
|
a4903a95be | ||
|
|
8c041095ab | ||
|
|
63dfdd133c | ||
|
|
b2be6f96c9 | ||
|
|
1dfc906802 | ||
|
|
d57313ae2c | ||
|
|
c125bcb1ca | ||
|
|
7dd9fde7ce | ||
|
|
f2f7d59577 | ||
|
|
7459e457bf | ||
|
|
145f98b53a | ||
|
|
6c58ea576e | ||
|
|
580ef30c8f | ||
|
|
94e9af6b2a | ||
|
|
3fb05a604f | ||
|
|
9b76ba19a8 | ||
|
|
1e7dd5dfd8 | ||
|
|
d6dec730d8 | ||
|
|
0ba28f3f91 | ||
|
|
55936e9366 | ||
|
|
6ecad4783f | ||
|
|
5759d08473 | ||
|
|
5048de80f0 | ||
|
|
97a437d059 | ||
|
|
cfb28ffdc0 | ||
|
|
ae9ef62f39 | ||
|
|
69feb7214a | ||
|
|
e95da82f5a | ||
|
|
7c5f9befb4 | ||
|
|
26a8d0c1c7 | ||
|
|
833815c71d | ||
|
|
31287b9b27 | ||
|
|
28f5b553a2 | ||
|
|
b33f173670 | ||
|
|
d8c6ee167c | ||
|
|
e288cb2771 | ||
|
|
5f2a6ebc2b | ||
|
|
cdc37c304a | ||
|
|
60e4d99b57 | ||
|
|
8229ffb674 | ||
|
|
9b944da896 | ||
|
|
5b37a6b04b | ||
|
|
9f18062d43 | ||
|
|
1055f28524 | ||
|
|
7b0021c1a8 | ||
|
|
ba997449aa | ||
|
|
ca2f0205c4 | ||
|
|
058525fe37 | ||
|
|
686ee31f8a | ||
|
|
767f235f94 | ||
|
|
d5a2c66746 | ||
|
|
f2ff083b8e | ||
|
|
8c45f25f33 | ||
|
|
f85cbfa9c8 | ||
|
|
71a3dad213 | ||
|
|
72e0535958 | ||
|
|
4f2d1c3a2a | ||
|
|
8e99cf7c93 | ||
|
|
2bb7f38603 | ||
|
|
0fe4449870 | ||
|
|
7c5fb2297c | ||
|
|
f4a811fbd3 | ||
|
|
bb3726bb87 | ||
|
|
333c8fe704 | ||
|
|
83213ce83f | ||
|
|
69718599ac | ||
|
|
0255957dd7 | ||
|
|
69b4b85cd9 | ||
|
|
a9b5be7ff4 | ||
|
|
1b46056e7d | ||
|
|
039d79b3c2 | ||
|
|
66b8aca399 | ||
|
|
41c413e178 | ||
|
|
d5f8720c4d | ||
|
|
e6eb9d79e3 | ||
|
|
b90d1738a9 | ||
|
|
f9e66dee9e |
12
.github/labels.yaml
vendored
12
.github/labels.yaml
vendored
@@ -44,12 +44,12 @@
|
|||||||
description: Feature request proposals in the RFC format
|
description: Feature request proposals in the RFC format
|
||||||
color: '#D621C3'
|
color: '#D621C3'
|
||||||
aliases: ['area/RFC']
|
aliases: ['area/RFC']
|
||||||
- name: backport:release/v2.4.x
|
|
||||||
description: To be backported to release/v2.4.x
|
|
||||||
color: '#ffd700'
|
|
||||||
- name: backport:release/v2.5.x
|
|
||||||
description: To be backported to release/v2.5.x
|
|
||||||
color: '#ffd700'
|
|
||||||
- name: backport:release/v2.6.x
|
- name: backport:release/v2.6.x
|
||||||
description: To be backported to release/v2.6.x
|
description: To be backported to release/v2.6.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
|
- name: backport:release/v2.7.x
|
||||||
|
description: To be backported to release/v2.7.x
|
||||||
|
color: '#ffd700'
|
||||||
|
- name: backport:release/v2.8.x
|
||||||
|
description: To be backported to release/v2.8.x
|
||||||
|
color: '#ffd700'
|
||||||
|
|||||||
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -23,7 +23,7 @@ amd when it finds a new controller version, the workflow performs the following
|
|||||||
- Updates the controller API package version in `go.mod`.
|
- Updates the controller API package version in `go.mod`.
|
||||||
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
||||||
- Patches the controller Deployment version in `manifests/bases` overlay.
|
- Patches the controller Deployment version in `manifests/bases` overlay.
|
||||||
- Opens a Pull Request against the `main` branch.
|
- Opens a Pull Request against the checked out branch.
|
||||||
- Triggers the e2e test suite to run for the opened PR.
|
- Triggers the e2e test suite to run for the opened PR.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/action.yaml
vendored
2
.github/workflows/action.yaml
vendored
@@ -24,6 +24,6 @@ jobs:
|
|||||||
name: action on ${{ matrix.version }}
|
name: action on ${{ matrix.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup flux
|
- name: Setup flux
|
||||||
uses: ./action
|
uses: ./action
|
||||||
|
|||||||
4
.github/workflows/backport.yaml
vendored
4
.github/workflows/backport.yaml
vendored
@@ -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.4.0
|
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
|||||||
40
.github/workflows/conformance.yaml
vendored
40
.github/workflows/conformance.yaml
vendored
@@ -3,13 +3,13 @@ name: conformance
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
|
branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.25.x
|
GO_VERSION: 1.26.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
conform-kubernetes:
|
conform-kubernetes:
|
||||||
@@ -19,13 +19,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
|
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
|
||||||
KUBERNETES_VERSION: [1.32.1, 1.33.0, 1.34.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
make build
|
make build
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.30.0
|
||||||
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
||||||
@@ -76,13 +76,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Available versions can be found with "replicated cluster versions"
|
# Available versions can be found with "replicated cluster versions"
|
||||||
K3S_VERSION: [ 1.32.8, 1.33.4 ]
|
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.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@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # 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@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/create-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "k3s"
|
kubernetes-distribution: "k3s"
|
||||||
@@ -120,8 +120,7 @@ jobs:
|
|||||||
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
|
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
|
||||||
- name: Run flux bootstrap
|
- name: Run flux bootstrap
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap git --manifests ./manifests/install/ \
|
./bin/flux bootstrap git --manifests ./manifests/test/ \
|
||||||
--components-extra=image-reflector-controller,image-automation-controller \
|
|
||||||
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
--path=clusters/k3s \
|
--path=clusters/k3s \
|
||||||
@@ -151,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@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/remove-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
@@ -169,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.19.0-okd ]
|
OPENSHIFT_VERSION: [ 4.20.0-okd ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -190,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@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -200,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@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/create-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "openshift"
|
kubernetes-distribution: "openshift"
|
||||||
@@ -212,7 +211,6 @@ jobs:
|
|||||||
- name: Run flux bootstrap
|
- name: Run flux bootstrap
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap git --manifests ./manifests/openshift/ \
|
./bin/flux bootstrap git --manifests ./manifests/openshift/ \
|
||||||
--components-extra=image-reflector-controller,image-automation-controller \
|
|
||||||
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
--path=clusters/openshift \
|
--path=clusters/openshift \
|
||||||
@@ -242,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@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/remove-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
|
|||||||
10
.github/workflows/e2e-azure.yaml
vendored
10
.github/workflows/e2e-azure.yaml
vendored
@@ -29,14 +29,14 @@ jobs:
|
|||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.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@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
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@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v1.4.6
|
uses: Azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # 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
|
||||||
|
|||||||
25
.github/workflows/e2e-bootstrap.yaml
vendored
25
.github/workflows/e2e-bootstrap.yaml
vendored
@@ -17,27 +17,27 @@ jobs:
|
|||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.30.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
# The versions below should target the newest Kubernetes version
|
# The versions below should target the newest Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||||
kubectl_version: v1.32.0
|
kubectl_version: v1.33.0
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Setup yq
|
- name: Setup yq
|
||||||
uses: fluxcd/pkg/actions/yq@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/yq@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
|
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
|
||||||
- name: bootstrap init
|
- name: bootstrap init
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--image-pull-secret=ghcr-auth \
|
--image-pull-secret=ghcr-auth \
|
||||||
--registry-creds=fluxcd:$GITHUB_TOKEN \
|
--registry-creds=fluxcd:$GITHUB_TOKEN \
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
|
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
|
||||||
- name: bootstrap no-op
|
- name: bootstrap no-op
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--image-pull-secret=ghcr-auth \
|
--image-pull-secret=ghcr-auth \
|
||||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
- name: bootstrap customize
|
- name: bootstrap customize
|
||||||
run: |
|
run: |
|
||||||
make setup-bootstrap-patch
|
make setup-bootstrap-patch
|
||||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
@@ -98,13 +98,14 @@ jobs:
|
|||||||
- name: test image automation
|
- name: test image automation
|
||||||
run: |
|
run: |
|
||||||
make setup-image-automation
|
make setup-image-automation
|
||||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
--path=test-cluster \
|
--path=test-cluster \
|
||||||
--read-write-key
|
--read-write-key
|
||||||
./bin/flux reconcile image repository podinfo
|
./bin/flux reconcile image repository podinfo
|
||||||
|
./bin/flux reconcile image policy podinfo
|
||||||
./bin/flux reconcile image update flux-system
|
./bin/flux reconcile image update flux-system
|
||||||
./bin/flux get images all
|
./bin/flux get images all
|
||||||
./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system
|
./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system
|
||||||
|
|||||||
14
.github/workflows/e2e-gcp.yaml
vendored
14
.github/workflows/e2e-gcp.yaml
vendored
@@ -29,14 +29,14 @@ jobs:
|
|||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.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@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
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@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Log into us-central1-docker.pkg.dev
|
- name: Log into us-central1-docker.pkg.dev
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: us-central1-docker.pkg.dev
|
registry: us-central1-docker.pkg.dev
|
||||||
username: oauth2accesstoken
|
username: oauth2accesstoken
|
||||||
|
|||||||
16
.github/workflows/e2e.yaml
vendored
16
.github/workflows/e2e.yaml
vendored
@@ -23,16 +23,16 @@ jobs:
|
|||||||
- 5000:5000
|
- 5000:5000
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.30.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
@@ -40,13 +40,13 @@ jobs:
|
|||||||
config: .github/kind/config.yaml # disable KIND-net
|
config: .github/kind/config.yaml # disable KIND-net
|
||||||
# The versions below should target the oldest supported Kubernetes version
|
# The versions below should target the oldest supported Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||||
kubectl_version: v1.32.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.27.3/manifests/calico.yaml
|
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
./bin/flux check --pre
|
./bin/flux check --pre
|
||||||
- name: flux install --manifests
|
- name: flux install --manifests
|
||||||
run: |
|
run: |
|
||||||
./bin/flux install --manifests ./manifests/install/
|
./bin/flux install --manifests ./manifests/test/
|
||||||
- name: flux create secret
|
- name: flux create secret
|
||||||
run: |
|
run: |
|
||||||
./bin/flux create secret git git-ssh-test \
|
./bin/flux create secret git git-ssh-test \
|
||||||
|
|||||||
8
.github/workflows/ossf.yaml
vendored
8
.github/workflows/ossf.yaml
vendored
@@ -19,21 +19,21 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Run analysis
|
- name: Run analysis
|
||||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
44
.github/workflows/release.yaml
vendored
44
.github/workflows/release.yaml
vendored
@@ -13,40 +13,44 @@ jobs:
|
|||||||
hashes: ${{ steps.slsa.outputs.hashes }}
|
hashes: ${{ steps.slsa.outputs.hashes }}
|
||||||
image_url: ${{ steps.slsa.outputs.image_url }}
|
image_url: ${{ steps.slsa.outputs.image_url }}
|
||||||
image_digest: ${{ steps.slsa.outputs.image_digest }}
|
image_digest: ${{ steps.slsa.outputs.image_digest }}
|
||||||
runs-on: ubuntu-latest
|
runs-on:
|
||||||
|
group: "Default Larger Runners"
|
||||||
|
labels: ubuntu-latest-16-cores
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # needed to write releases
|
contents: write # needed to write releases
|
||||||
id-token: write # needed for keyless signing
|
id-token: write # needed for keyless signing
|
||||||
packages: write # needed for ghcr access
|
packages: write # needed for ghcr access
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
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@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache: false
|
cache: false
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Setup Syft
|
- name: Setup Syft
|
||||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
|
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
- name: Setup Cosign
|
- name: Setup Cosign
|
||||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
with:
|
||||||
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
@@ -59,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@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/crdjsonschema@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
with:
|
with:
|
||||||
crd: all-crds.yaml
|
crd: all-crds.yaml
|
||||||
output: schemas
|
output: schemas
|
||||||
@@ -68,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@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --skip=validate
|
args: release --skip=validate
|
||||||
@@ -99,24 +103,26 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
uses: ./action/
|
uses: ./action/
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
id: prep
|
id: prep
|
||||||
run: |
|
run: |
|
||||||
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@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
@@ -144,7 +150,9 @@ 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@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
with:
|
||||||
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Sign manifests
|
- name: Sign manifests
|
||||||
env:
|
env:
|
||||||
COSIGN_EXPERIMENTAL: 1
|
COSIGN_EXPERIMENTAL: 1
|
||||||
|
|||||||
2
.github/workflows/scan.yaml
vendored
2
.github/workflows/scan.yaml
vendored
@@ -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.4.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 }}
|
||||||
|
|||||||
2
.github/workflows/sync-labels.yaml
vendored
2
.github/workflows/sync-labels.yaml
vendored
@@ -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.4.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 }}
|
||||||
|
|||||||
10
.github/workflows/update.yaml
vendored
10
.github/workflows/update.yaml
vendored
@@ -16,11 +16,11 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
commit-message: |
|
commit-message: |
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
committer: GitHub <noreply@github.com>
|
committer: GitHub <noreply@github.com>
|
||||||
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
||||||
signoff: true
|
signoff: true
|
||||||
branch: update-components
|
branch: update-components-${{ github.ref_name }}
|
||||||
title: Update toolkit components
|
title: Update toolkit components
|
||||||
body: |
|
body: |
|
||||||
${{ steps.update.outputs.pr_body }}
|
${{ steps.update.outputs.pr_body }}
|
||||||
|
|||||||
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
Normal file
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: upgrade-fluxcd-pkg
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upgrade-fluxcd-pkg:
|
||||||
|
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
|
||||||
|
secrets:
|
||||||
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
@@ -88,22 +88,6 @@ brews:
|
|||||||
generate_completions_from_executable(bin/"flux", "completion")
|
generate_completions_from_executable(bin/"flux", "completion")
|
||||||
test: |
|
test: |
|
||||||
system "#{bin}/flux --version"
|
system "#{bin}/flux --version"
|
||||||
publishers:
|
|
||||||
- name: aur-pkg-bin
|
|
||||||
env:
|
|
||||||
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
|
||||||
cmd: |
|
|
||||||
.github/aur/flux-bin/publish.sh {{ .Version }}
|
|
||||||
- name: aur-pkg-scm
|
|
||||||
env:
|
|
||||||
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
|
||||||
cmd: |
|
|
||||||
.github/aur/flux-scm/publish.sh {{ .Version }}
|
|
||||||
- name: aur-pkg-go
|
|
||||||
env:
|
|
||||||
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
|
||||||
cmd: |
|
|
||||||
.github/aur/flux-go/publish.sh {{ .Version }}
|
|
||||||
dockers:
|
dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- 'fluxcd/flux-cli:{{ .Tag }}-amd64'
|
- 'fluxcd/flux-cli:{{ .Tag }}-amd64'
|
||||||
|
|||||||
151
AGENTS.md
Normal file
151
AGENTS.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# 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.
|
||||||
235
CONTRIBUTING.md
235
CONTRIBUTING.md
@@ -1,154 +1,129 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and
|
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and accepts contributions via GitHub pull requests.
|
||||||
accepts contributions via GitHub pull requests. This document outlines
|
This document outlines the conventions to get your contribution accepted.
|
||||||
some of the conventions on to make it easier to get your contribution
|
We gratefully welcome improvements to documentation as well as code contributions.
|
||||||
accepted.
|
|
||||||
|
|
||||||
We gratefully welcome improvements to issues and documentation as well as to
|
If you are new to the project, we recommend starting with documentation improvements or
|
||||||
code.
|
small bug fixes to get familiar with the codebase and the contribution process.
|
||||||
|
|
||||||
|
## 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
|
By contributing to this project you agree to the Developer Certificate of Origin (DCO).
|
||||||
Origin (DCO). This document was created by the Linux Kernel community and is a
|
This document was created by the Linux Kernel community and is a simple statement that you,
|
||||||
simple statement that you, as a contributor, have the legal right to make the
|
as a contributor, have the legal right to make the contribution.
|
||||||
contribution.
|
|
||||||
|
|
||||||
We require all commits to be signed. By signing off with your signature, you
|
We require all commits to be signed. By signing off with your signature, you certify that you wrote
|
||||||
certify that you wrote the patch or otherwise have the right to contribute the
|
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):
|
||||||
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
|
The signature must contain your real name (sorry, no pseudonyms or anonymous contributions).
|
||||||
(sorry, no pseudonyms or anonymous contributions)
|
If your `user.name` and `user.email` are set in your Git config,
|
||||||
If your `user.name` and `user.email` are configured in your Git config,
|
|
||||||
you can sign your commit automatically with `git commit -s`.
|
you can sign your commit automatically with `git commit -s`.
|
||||||
|
|
||||||
## Communications
|
|
||||||
|
|
||||||
For realtime communications we use Slack: To join the conversation, simply
|
|
||||||
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the
|
|
||||||
[#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
|
|
||||||
|
|
||||||
To discuss ideas and specifications we use [Github
|
|
||||||
Discussions](https://github.com/fluxcd/flux2/discussions).
|
|
||||||
|
|
||||||
For announcements we use a mailing list as well. Simply subscribe to
|
|
||||||
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev)
|
|
||||||
to join the conversation (there you can also add calendar invites
|
|
||||||
to your Google calendar for our [Flux
|
|
||||||
meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)).
|
|
||||||
|
|
||||||
## Understanding Flux and the GitOps Toolkit
|
|
||||||
|
|
||||||
If you are entirely new to Flux and the GitOps Toolkit,
|
|
||||||
you might want to take a look at the [introductory talk and demo](https://www.youtube.com/watch?v=qQBtSkgl7tI).
|
|
||||||
|
|
||||||
This project is composed of:
|
|
||||||
|
|
||||||
- [flux2](https://github.com/fluxcd/flux2): The Flux CLI
|
|
||||||
- [source-manager](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git and Helm repositories, S3-compatible Buckets)
|
|
||||||
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
|
||||||
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
|
|
||||||
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
|
|
||||||
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
|
|
||||||
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
|
|
||||||
|
|
||||||
### Understanding the code
|
|
||||||
|
|
||||||
To get started with developing controllers, you might want to review
|
|
||||||
[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
|
|
||||||
walks you through writing a short and concise controller that watches out
|
|
||||||
for source changes.
|
|
||||||
|
|
||||||
## How to run the test suite
|
|
||||||
|
|
||||||
Prerequisites:
|
|
||||||
|
|
||||||
* go >= 1.24
|
|
||||||
* kubectl >= 1.30
|
|
||||||
* kustomize >= 5.0
|
|
||||||
* coreutils (on Mac OS)
|
|
||||||
|
|
||||||
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install-envtest
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run the unit tests with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
|
|
||||||
create a cluster for testing with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make setup-kind
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run the end-to-end tests with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
When the output of the Flux CLI changes, to automatically update the golden
|
|
||||||
files used in the test, pass `-update` flag to the test as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make e2e TEST_ARGS="-update"
|
|
||||||
```
|
|
||||||
|
|
||||||
Since not all packages use golden files for testing, `-update` argument must be
|
|
||||||
passed only for the packages that use golden files. Use the variables
|
|
||||||
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
|
|
||||||
path of the target test package:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unit test
|
|
||||||
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
|
||||||
# e2e test
|
|
||||||
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
|
||||||
```
|
|
||||||
|
|
||||||
Teardown the e2e environment with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make cleanup-kind
|
|
||||||
```
|
|
||||||
|
|
||||||
## Acceptance policy
|
## Acceptance policy
|
||||||
|
|
||||||
These things will make a PR more likely to be accepted:
|
These things will make a PR more likely to be accepted:
|
||||||
|
|
||||||
- a well-described requirement
|
- 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.
|
||||||
- tests for new code
|
- 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 old 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.
|
||||||
- new code and tests follow the conventions in old code and tests
|
- All top-level Go code and exported names should have doc comments, as should non-trivial unexported type or function declarations.
|
||||||
- a good commit message (see below)
|
- 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`.
|
||||||
- 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 GitOps Toolkit controllers we prefer the following rules for good commit messages:
|
For the Flux project we prefer the following rules:
|
||||||
|
|
||||||
- Limit the subject to 50 characters and write as the continuation
|
- Limit the subject to 50 characters, start with a capital letter and do not end with a period.
|
||||||
of the sentence "If applied, this commit will ..."
|
- Explain what and why in the body, if more than a trivial change; wrap it at 72 characters.
|
||||||
- Explain what and why in the body, if more than a trivial change;
|
- 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").
|
||||||
wrap it at 72 characters.
|
- Do not include GitHub mentions to issues in the commit message, use the PR description instead (e.g., "Fixes #123" or "Closes #123").
|
||||||
|
- Do not include GitHub mentions to accounts (e.g., `@username` or `@team`) within the commit message.
|
||||||
|
|
||||||
The [following article](https://chris.beams.io/posts/git-commit/#seven-rules)
|
## Pull Request Process
|
||||||
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).
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
FROM alpine:3.22 AS builder
|
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.34.0
|
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
|
||||||
|
|
||||||
RUN kubectl version --client=true
|
RUN kubectl version --client=true
|
||||||
|
|
||||||
FROM alpine:3.22 AS flux-cli
|
FROM alpine:3.23 AS flux-cli
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
|
|||||||
all: test build
|
all: test build
|
||||||
|
|
||||||
tidy:
|
tidy:
|
||||||
go mod tidy -compat=1.25
|
go mod tidy -compat=1.26
|
||||||
cd tests/integration && go mod tidy -compat=1.25
|
cd tests/integration && go mod tidy -compat=1.26
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|||||||
@@ -52,12 +52,14 @@ guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/).
|
|||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
- [Source Controller](https://fluxcd.io/flux/components/source/)
|
- [Source Controllers](https://fluxcd.io/flux/components/source/)
|
||||||
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
|
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
|
||||||
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
|
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
|
||||||
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
|
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
|
||||||
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
|
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
|
||||||
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
|
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
|
||||||
|
- [ExternalArtifact CRD](https://fluxcd.io/flux/components/source/externalartifacts/)
|
||||||
|
- [ArtifactGenerator CRD](https://fluxcd.io/flux/components/source/artifactgenerators/)
|
||||||
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
|
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
|
||||||
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
|
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
|
||||||
- [Helm Controller](https://fluxcd.io/flux/components/helm/)
|
- [Helm Controller](https://fluxcd.io/flux/components/helm/)
|
||||||
|
|||||||
@@ -77,8 +77,36 @@ runs:
|
|||||||
|
|
||||||
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
|
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
|
||||||
|
|
||||||
curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"
|
MAX_RETRIES=5
|
||||||
curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"
|
RETRY_DELAY=5
|
||||||
|
|
||||||
|
for i in $(seq 1 $MAX_RETRIES); do
|
||||||
|
echo "Downloading flux binary (attempt $i/$MAX_RETRIES)"
|
||||||
|
if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -lt $MAX_RETRIES ]; then
|
||||||
|
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
|
||||||
|
sleep $RETRY_DELAY
|
||||||
|
else
|
||||||
|
echo "Failed to download flux binary after $MAX_RETRIES attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in $(seq 1 $MAX_RETRIES); do
|
||||||
|
echo "Downloading checksums file (attempt $i/$MAX_RETRIES)"
|
||||||
|
if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -lt $MAX_RETRIES ]; then
|
||||||
|
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
|
||||||
|
sleep $RETRY_DELAY
|
||||||
|
else
|
||||||
|
echo "Failed to download checksums file after $MAX_RETRIES attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo "Verifying checksum"
|
echo "Verifying checksum"
|
||||||
sum=""
|
sum=""
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -51,6 +52,7 @@ 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, ",")...)
|
||||||
@@ -61,6 +63,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -85,6 +88,15 @@ 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())
|
||||||
@@ -96,6 +108,141 @@ 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 {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -68,3 +69,149 @@ 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"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ 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
|
||||||
@@ -84,6 +86,8 @@ func init() {
|
|||||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||||
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
|
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
|
||||||
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
||||||
|
buildKsCmd.Flags().BoolVar(&buildKsArgs.inMemoryBuild, "in-memory-build", true,
|
||||||
|
"Use in-memory filesystem during build.")
|
||||||
buildCmd.AddCommand(buildKsCmd)
|
buildCmd.AddCommand(buildKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +101,13 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
|||||||
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize the path to handle Windows absolute and relative paths correctly
|
||||||
|
buildKsArgs.path, err = filepath.Abs(buildKsArgs.path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve absolute path: %w", err)
|
||||||
|
}
|
||||||
|
buildKsArgs.path = filepath.Clean(buildKsArgs.path)
|
||||||
|
|
||||||
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
|
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
|
||||||
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
||||||
}
|
}
|
||||||
@@ -122,6 +133,7 @@ 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,
|
||||||
@@ -132,6 +144,7 @@ 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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ func TestBuildKustomization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build podinfo (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
||||||
@@ -70,12 +76,24 @@ func TestBuildKustomization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build ignore (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build with recursive",
|
name: "build with recursive",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
@@ -145,6 +163,12 @@ spec:
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build podinfo (on-disk)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
||||||
@@ -175,6 +199,18 @@ spec:
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive (on-disk)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive in dry-run mode (on-disk)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false --dry-run",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
@@ -218,3 +254,71 @@ spec:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildKustomizationPathNormalization verifies that absolute and complex
|
||||||
|
// paths are normalized to prevent path concatenation bugs (issue #5673).
|
||||||
|
// Without normalization, paths could be duplicated like: /path/test/path/test/file
|
||||||
|
func TestBuildKustomizationPathNormalization(t *testing.T) {
|
||||||
|
// Get absolute path to testdata to test absolute path handling
|
||||||
|
absTestDataPath, err := filepath.Abs("testdata/build-kustomization/podinfo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get absolute path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
resultFile string
|
||||||
|
assertFunc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "build with absolute path",
|
||||||
|
args: "build kustomization podinfo --path " + absTestDataPath,
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build with absolute path (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build with complex relative path (parent dir)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build with path containing redundant separators",
|
||||||
|
args: "build kustomization podinfo --path ./testdata//build-kustomization//podinfo",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setup(t, tmpl)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var assert assertFunc
|
||||||
|
|
||||||
|
switch tt.assertFunc {
|
||||||
|
case "assertGoldenTemplateFile":
|
||||||
|
assert = assertGoldenTemplateFile(tt.resultFile, tmpl)
|
||||||
|
case "assertError":
|
||||||
|
assert = assertError(tt.resultFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args + " -n " + tmpl["fluxns"],
|
||||||
|
assert: assert,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ type checkFlags struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kubernetesConstraints = []string{
|
var kubernetesConstraints = []string{
|
||||||
">=1.32.0-0",
|
">=1.33.0-0",
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkArgs checkFlags
|
var checkArgs checkFlags
|
||||||
|
|||||||
@@ -182,6 +182,10 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("chart or chart-ref is required")
|
return fmt.Errorf("chart or chart-ref is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if helmReleaseArgs.chart != "" && helmReleaseArgs.chartRef != "" {
|
||||||
|
return fmt.Errorf("cannot use --chart in combination with --chart-ref")
|
||||||
|
}
|
||||||
|
|
||||||
sourceLabels, err := parseLabels()
|
sourceLabels, err := parseLabels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ func TestCreateHelmRelease(t *testing.T) {
|
|||||||
args: "create helmrelease podinfo --export",
|
args: "create helmrelease podinfo --export",
|
||||||
assert: assertError("chart or chart-ref is required"),
|
assert: assertError("chart or chart-ref is required"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "chart and chartRef used in combination",
|
||||||
|
args: "create helmrelease podinfo --chart podinfo --chart-ref foobar/podinfo --export",
|
||||||
|
assert: assertError("cannot use --chart in combination with --chart-ref"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unknown source kind",
|
name: "unknown source kind",
|
||||||
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
|
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
|
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
|
||||||
return fmt.Errorf("path must begin with ./")
|
return fmt.Errorf("path must begin with ./")
|
||||||
}
|
}
|
||||||
|
if kustomizationArgs.source.Name == "" {
|
||||||
|
return fmt.Errorf("source is required")
|
||||||
|
}
|
||||||
|
|
||||||
if !createArgs.export {
|
if !createArgs.export {
|
||||||
logger.Generatef("generating Kustomization")
|
logger.Generatef("generating Kustomization")
|
||||||
|
|||||||
48
cmd/flux/create_kustomization_test.go
Normal file
48
cmd/flux/create_kustomization_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//go:build unit
|
||||||
|
// +build unit
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCreateKustomization(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// A user creating a kustomization without --source gets a confusing
|
||||||
|
// API-level error about spec.sourceRef.kind instead of a clear message.
|
||||||
|
name: "missing source",
|
||||||
|
args: "create kustomization my-app --path=./deploy --export",
|
||||||
|
assert: assertError("source is required"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ var createReceiverCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type receiverFlags struct {
|
type receiverFlags struct {
|
||||||
receiverType string
|
receiverType flags.ReceiverType
|
||||||
secretRef string
|
secretRef string
|
||||||
events []string
|
events []string
|
||||||
resources []string
|
resources []string
|
||||||
@@ -58,7 +59,7 @@ type receiverFlags struct {
|
|||||||
var receiverArgs receiverFlags
|
var receiverArgs receiverFlags
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "")
|
createReceiverCmd.Flags().Var(&receiverArgs.receiverType, "type", receiverArgs.receiverType.Description())
|
||||||
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
||||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
||||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
||||||
@@ -109,7 +110,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Labels: sourceLabels,
|
Labels: sourceLabels,
|
||||||
},
|
},
|
||||||
Spec: notificationv1.ReceiverSpec{
|
Spec: notificationv1.ReceiverSpec{
|
||||||
Type: receiverArgs.receiverType,
|
Type: receiverArgs.receiverType.String(),
|
||||||
Events: receiverArgs.events,
|
Events: receiverArgs.events,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
SecretRef: meta.LocalObjectReference{
|
SecretRef: meta.LocalObjectReference{
|
||||||
|
|||||||
@@ -56,6 +56,22 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
existing.StringData = secret.StringData
|
existing.StringData = secret.StringData
|
||||||
|
if secret.Annotations != nil {
|
||||||
|
if existing.Annotations == nil {
|
||||||
|
existing.Annotations = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range secret.Annotations {
|
||||||
|
existing.Annotations[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if secret.Labels != nil {
|
||||||
|
if existing.Labels == nil {
|
||||||
|
existing.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range secret.Labels {
|
||||||
|
existing.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := kubeClient.Update(ctx, &existing); err != nil {
|
if err := kubeClient.Update(ctx, &existing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ var createSecretGitHubAppCmd = &cobra.Command{
|
|||||||
|
|
||||||
type secretGitHubAppFlags struct {
|
type secretGitHubAppFlags struct {
|
||||||
appID string
|
appID string
|
||||||
|
appInstallationOwner string
|
||||||
appInstallationID string
|
appInstallationID string
|
||||||
privateKeyFile string
|
privateKeyFile string
|
||||||
baseURL string
|
baseURL string
|
||||||
@@ -56,6 +57,7 @@ var secretGitHubAppArgs = secretGitHubAppFlags{}
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
|
||||||
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationOwner, "app-installation-owner", "", "github app installation owner (user or organization)")
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
|
||||||
@@ -70,18 +72,6 @@ func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
secretName := args[0]
|
secretName := args[0]
|
||||||
|
|
||||||
if secretGitHubAppArgs.appID == "" {
|
|
||||||
return fmt.Errorf("--app-id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if secretGitHubAppArgs.appInstallationID == "" {
|
|
||||||
return fmt.Errorf("--app-installation-id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if secretGitHubAppArgs.privateKeyFile == "" {
|
|
||||||
return fmt.Errorf("--app-private-key is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
|
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read private key file: %w", err)
|
return fmt.Errorf("unable to read private key file: %w", err)
|
||||||
@@ -91,12 +81,10 @@ func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Name: secretName,
|
Name: secretName,
|
||||||
Namespace: *kubeconfigArgs.Namespace,
|
Namespace: *kubeconfigArgs.Namespace,
|
||||||
GitHubAppID: secretGitHubAppArgs.appID,
|
GitHubAppID: secretGitHubAppArgs.appID,
|
||||||
|
GitHubAppInstallationOwner: secretGitHubAppArgs.appInstallationOwner,
|
||||||
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
|
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
|
||||||
GitHubAppPrivateKey: string(privateKey),
|
GitHubAppPrivateKey: string(privateKey),
|
||||||
}
|
GitHubAppBaseURL: secretGitHubAppArgs.baseURL,
|
||||||
|
|
||||||
if secretGitHubAppArgs.baseURL != "" {
|
|
||||||
opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := sourcesecret.GenerateGitHubApp(opts)
|
secret, err := sourcesecret.GenerateGitHubApp(opts)
|
||||||
|
|||||||
@@ -31,21 +31,6 @@ func TestCreateSecretGitHubApp(t *testing.T) {
|
|||||||
args: "create secret githubapp",
|
args: "create secret githubapp",
|
||||||
assert: assertError("name is required"),
|
assert: assertError("name is required"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "create githubapp secret with missing app-id",
|
|
||||||
args: "create secret githubapp appinfo",
|
|
||||||
assert: assertError("--app-id is required"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create githubapp secret with missing appInstallationID",
|
|
||||||
args: "create secret githubapp appinfo --app-id 1",
|
|
||||||
assert: assertError("--app-installation-id is required"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create githubapp secret with missing private key file",
|
|
||||||
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2",
|
|
||||||
assert: assertError("--app-private-key is required"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "create githubapp secret with private key file that does not exist",
|
name: "create githubapp secret with private key file that does not exist",
|
||||||
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
|
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
|
||||||
@@ -53,7 +38,7 @@ func TestCreateSecretGitHubApp(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create githubapp secret with app info",
|
name: "create githubapp secret with app info",
|
||||||
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
|
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-owner my-org --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
|
||||||
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
|
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
134
cmd/flux/create_secret_receiver.go
Normal file
134
cmd/flux/create_secret_receiver.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
var createSecretReceiverCmd = &cobra.Command{
|
||||||
|
Use: "receiver [name]",
|
||||||
|
Short: "Create or update a Kubernetes secret for a Receiver webhook",
|
||||||
|
Long: `The create secret receiver command generates a Kubernetes secret with
|
||||||
|
the token used for webhook payload validation and an annotation with the
|
||||||
|
computed webhook URL.`,
|
||||||
|
Example: ` # Create a receiver secret for a GitHub webhook
|
||||||
|
flux create secret receiver github-receiver \
|
||||||
|
--namespace=my-namespace \
|
||||||
|
--type=github \
|
||||||
|
--hostname=flux.example.com \
|
||||||
|
--export
|
||||||
|
|
||||||
|
# Create a receiver secret for GCR with email claim
|
||||||
|
flux create secret receiver gcr-receiver \
|
||||||
|
--namespace=my-namespace \
|
||||||
|
--type=gcr \
|
||||||
|
--hostname=flux.example.com \
|
||||||
|
--email-claim=sa@project.iam.gserviceaccount.com \
|
||||||
|
--export`,
|
||||||
|
RunE: createSecretReceiverCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretReceiverFlags struct {
|
||||||
|
receiverType flags.ReceiverType
|
||||||
|
token string
|
||||||
|
hostname string
|
||||||
|
emailClaim string
|
||||||
|
audienceClaim string
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretReceiverArgs secretReceiverFlags
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
createSecretReceiverCmd.Flags().Var(&secretReceiverArgs.receiverType, "type", secretReceiverArgs.receiverType.Description())
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.token, "token", "", "webhook token used for payload validation and URL computation, auto-generated if not specified")
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.hostname, "hostname", "", "hostname for the webhook URL e.g. flux.example.com")
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.emailClaim, "email-claim", "", "IAM service account email, required for gcr type")
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.audienceClaim, "audience-claim", "", "custom OIDC token audience for gcr type, defaults to the webhook URL")
|
||||||
|
|
||||||
|
createSecretCmd.AddCommand(createSecretReceiverCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSecretReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
if secretReceiverArgs.receiverType == "" {
|
||||||
|
return fmt.Errorf("--type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretReceiverArgs.hostname == "" {
|
||||||
|
return fmt.Errorf("--hostname is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretReceiverArgs.receiverType.String() == notificationv1.GCRReceiver && secretReceiverArgs.emailClaim == "" {
|
||||||
|
return fmt.Errorf("--email-claim is required for gcr receiver type")
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, err := parseLabels()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := sourcesecret.Options{
|
||||||
|
Name: name,
|
||||||
|
Namespace: *kubeconfigArgs.Namespace,
|
||||||
|
Labels: labels,
|
||||||
|
ReceiverType: secretReceiverArgs.receiverType.String(),
|
||||||
|
Token: secretReceiverArgs.token,
|
||||||
|
Hostname: secretReceiverArgs.hostname,
|
||||||
|
EmailClaim: secretReceiverArgs.emailClaim,
|
||||||
|
AudienceClaim: secretReceiverArgs.audienceClaim,
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := sourcesecret.GenerateReceiver(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if createArgs.export {
|
||||||
|
rootCmd.Println(secret.Content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||||
|
defer cancel()
|
||||||
|
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var s corev1.Secret
|
||||||
|
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertSecret(ctx, kubeClient, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Actionf("receiver secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
74
cmd/flux/create_secret_receiver_test.go
Normal file
74
cmd/flux/create_secret_receiver_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateReceiverSecret(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing type",
|
||||||
|
args: "create secret receiver test-secret --token=t --hostname=h",
|
||||||
|
assert: assertError("--type is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid type",
|
||||||
|
args: "create secret receiver test-secret --type=invalid --token=t --hostname=h",
|
||||||
|
assert: assertError("invalid argument \"invalid\" for \"--type\" flag: receiver type 'invalid' is not supported, must be one of: generic, generic-hmac, github, gitlab, bitbucket, harbor, dockerhub, quay, gcr, nexus, acr, cdevents"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing hostname",
|
||||||
|
args: "create secret receiver test-secret --type=github --token=t",
|
||||||
|
assert: assertError("--hostname is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcr missing email-claim",
|
||||||
|
args: "create secret receiver test-secret --type=gcr --token=t --hostname=h",
|
||||||
|
assert: assertError("--email-claim is required for gcr receiver type"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "github receiver secret",
|
||||||
|
args: "create secret receiver receiver-secret --type=github --token=test-token --hostname=flux.example.com --namespace=my-namespace --export",
|
||||||
|
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcr receiver secret",
|
||||||
|
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --namespace=my-namespace --export",
|
||||||
|
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcr receiver secret with custom audience",
|
||||||
|
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --audience-claim=https://custom.audience.example.com --namespace=my-namespace --export",
|
||||||
|
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ type tenantFlags struct {
|
|||||||
namespaces []string
|
namespaces []string
|
||||||
clusterRole string
|
clusterRole string
|
||||||
account string
|
account string
|
||||||
|
skipNamespace bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenantArgs tenantFlags
|
var tenantArgs tenantFlags
|
||||||
@@ -69,6 +70,7 @@ func init() {
|
|||||||
createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant")
|
createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant")
|
||||||
createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding")
|
createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding")
|
||||||
createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant")
|
createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant")
|
||||||
|
createTenantCmd.Flags().BoolVar(&tenantArgs.skipNamespace, "skip-namespace", false, "skip namespace creation (namespace must exist already)")
|
||||||
createCmd.AddCommand(createTenantCmd)
|
createCmd.AddCommand(createTenantCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +159,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if createArgs.export {
|
if createArgs.export {
|
||||||
for i := range tenantArgs.namespaces {
|
for i := range tenantArgs.namespaces {
|
||||||
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i]); err != nil {
|
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i], tenantArgs.skipNamespace); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,10 +175,12 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range tenantArgs.namespaces {
|
for i := range tenantArgs.namespaces {
|
||||||
|
if !tenantArgs.skipNamespace {
|
||||||
logger.Actionf("applying namespace %s", namespaces[i].Name)
|
logger.Actionf("applying namespace %s", namespaces[i].Name)
|
||||||
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
|
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Actionf("applying service account %s", accounts[i].Name)
|
logger.Actionf("applying service account %s", accounts[i].Name)
|
||||||
if err := upsertServiceAccount(ctx, kubeClient, accounts[i]); err != nil {
|
if err := upsertServiceAccount(ctx, kubeClient, accounts[i]); err != nil {
|
||||||
@@ -284,12 +288,16 @@ func upsertRoleBinding(ctx context.Context, kubeClient client.Client, roleBindin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding) error {
|
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding, skipNamespace bool) error {
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if !skipNamespace {
|
||||||
namespace.TypeMeta = metav1.TypeMeta{
|
namespace.TypeMeta = metav1.TypeMeta{
|
||||||
APIVersion: "v1",
|
APIVersion: "v1",
|
||||||
Kind: "Namespace",
|
Kind: "Namespace",
|
||||||
}
|
}
|
||||||
data, err := yaml.Marshal(namespace)
|
data, err = yaml.Marshal(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -297,6 +305,7 @@ func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, rol
|
|||||||
|
|
||||||
printlnStdout("---")
|
printlnStdout("---")
|
||||||
printlnStdout(resourceToString(data))
|
printlnStdout(resourceToString(data))
|
||||||
|
}
|
||||||
|
|
||||||
account.TypeMeta = metav1.TypeMeta{
|
account.TypeMeta = metav1.TypeMeta{
|
||||||
APIVersion: "v1",
|
APIVersion: "v1",
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ func TestCreateTenant(t *testing.T) {
|
|||||||
args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export",
|
args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export",
|
||||||
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"),
|
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "tenant with skip namespace",
|
||||||
|
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --skip-namespace --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-skip-namespace.yaml"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if diffArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
if diffArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
opt, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String())
|
opt, _, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ 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
|
||||||
@@ -75,6 +77,10 @@ func init() {
|
|||||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||||
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
|
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
|
||||||
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
||||||
|
diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true,
|
||||||
|
"Use in-memory filesystem during build.")
|
||||||
|
diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false,
|
||||||
|
"Ignore Kustomization not found errors on the cluster when diffing.")
|
||||||
diffCmd.AddCommand(diffKsCmd)
|
diffCmd.AddCommand(diffKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +119,8 @@ 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,
|
||||||
@@ -124,6 +132,8 @@ 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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
name: "diff nothing deployed",
|
name: "diff nothing deployed",
|
||||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
||||||
objectFile: "",
|
objectFile: "",
|
||||||
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "diff with a deployment object",
|
name: "diff with a deployment object",
|
||||||
@@ -96,7 +96,7 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
name: "diff where kustomization file has multiple objects with the same name",
|
name: "diff where kustomization file has multiple objects with the same name",
|
||||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
|
||||||
objectFile: "",
|
objectFile: "",
|
||||||
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "diff with recursive",
|
name: "diff with recursive",
|
||||||
@@ -138,6 +138,118 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNotDeployed tests `flux diff ks` when the Kustomization
|
||||||
|
// CR does not exist in the cluster but is provided via --kustomization-file.
|
||||||
|
// Reproduces https://github.com/fluxcd/flux2/issues/5439
|
||||||
|
func TestDiffKustomizationNotDeployed(t *testing.T) {
|
||||||
|
// Use a dedicated namespace with NO setup() -- the Kustomization CR
|
||||||
|
// intentionally does not exist in the cluster.
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fails without --ignore-not-found",
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml",
|
||||||
|
assert: assertError("failed to get kustomization object: kustomizations.kustomize.toolkit.fluxcd.io \"podinfo\" not found"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "succeeds with --ignore-not-found and --kustomization-file",
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml " +
|
||||||
|
"--ignore-not-found",
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args + " -n " + tmpl["fluxns"],
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationTakeOwnership tests `flux diff ks` when taking ownership
|
||||||
|
// of existing resources on the cluster. A "pre-existing" configmap is applied
|
||||||
|
// to the cluster, and the kustomization contains a matching configmap; the
|
||||||
|
// diff should show the labels added by flux
|
||||||
|
func TestDiffKustomizationTakeOwnership(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
b, _ := build.NewBuilder("configmaps", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
|
||||||
|
resourceManager, err := b.Manager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-create the "existing" configmap in the cluster without Flux labels
|
||||||
|
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/existing-configmap.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization configmaps --path ./testdata/build-kustomization/configmaps --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-configmaps.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-taking-ownership.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNewNamespaceAndConfigmap runs `flux diff ks` when the
|
||||||
|
// kustomization creates a new namespace and resources inside it. The server-side
|
||||||
|
// dry-run cannot resolve resources in a namespace that doesn't exist yet,
|
||||||
|
// consistent with `kubectl diff --server-side` behavior.
|
||||||
|
func TestDiffKustomizationNewNamespaceAndConfigmap(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization new-namespace-and-configmap --path ./testdata/build-kustomization/new-namespace-and-configmap --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertError("ConfigMap/new-ns/app-config not found: namespaces \"new-ns\" not found"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNewNamespaceOnly runs `flux diff ks` when the
|
||||||
|
// kustomization creates only a new namespace. The diff should show the
|
||||||
|
// namespace as created.
|
||||||
|
func TestDiffKustomizationNewNamespaceOnly(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization new-namespace-only --path ./testdata/build-kustomization/new-namespace-only --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-namespace-only.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
||||||
buf, err := os.ReadFile(objectFile)
|
buf, err := os.ReadFile(objectFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -196,11 +196,14 @@ func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []cli
|
|||||||
|
|
||||||
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
||||||
listOpts := &metav1.ListOptions{}
|
listOpts := &metav1.ListOptions{}
|
||||||
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
|
||||||
err := runtimeresource.FollowContinue(listOpts,
|
err := runtimeresource.FollowContinue(listOpts,
|
||||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||||
newEvents := &corev1.EventList{}
|
newEvents := &corev1.EventList{}
|
||||||
if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
|
opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||||
|
if options.Continue != "" {
|
||||||
|
opts = append(opts, client.Continue(options.Continue))
|
||||||
|
}
|
||||||
|
if err := kubeclient.List(ctx, newEvents, opts...); err != nil {
|
||||||
return nil, fmt.Errorf("error getting events: %w", err)
|
return nil, fmt.Errorf("error getting events: %w", err)
|
||||||
}
|
}
|
||||||
el.Items = append(el.Items, newEvents.Items...)
|
el.Items = append(el.Items, newEvents.Items...)
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
@@ -419,6 +421,108 @@ func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paginatedClient wraps a client.Client and simulates real Kubernetes API
|
||||||
|
// pagination by splitting List results into pages of pageSize items,
|
||||||
|
// using the ListMeta.Continue token.
|
||||||
|
type paginatedClient struct {
|
||||||
|
client.Client
|
||||||
|
pageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paginatedClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||||
|
listOpts := &client.ListOptions{}
|
||||||
|
listOpts.ApplyOptions(opts)
|
||||||
|
|
||||||
|
// Fetch all results from the underlying client (without Limit/Continue).
|
||||||
|
stripped := make([]client.ListOption, 0, len(opts))
|
||||||
|
for _, o := range opts {
|
||||||
|
if _, ok := o.(client.Limit); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := o.(client.Continue); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stripped = append(stripped, o)
|
||||||
|
}
|
||||||
|
if err := c.Client.List(ctx, list, stripped...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := meta.ExtractList(list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the page window based on the Continue token.
|
||||||
|
start := 0
|
||||||
|
if listOpts.Continue != "" {
|
||||||
|
n, err := strconv.Atoi(listOpts.Continue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid continue token: %w", err)
|
||||||
|
}
|
||||||
|
start = n
|
||||||
|
}
|
||||||
|
if start > len(items) {
|
||||||
|
start = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
end := start + c.pageSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := items[start:end]
|
||||||
|
if err := meta.SetList(list, page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the Continue token when there are more pages.
|
||||||
|
listAccessor, err := meta.ListAccessor(list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if end < len(items) {
|
||||||
|
listAccessor.SetContinue(strconv.Itoa(end))
|
||||||
|
} else {
|
||||||
|
listAccessor.SetContinue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_addEventsToList_pagination(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
objs, err := ssautil.ReadObjects(strings.NewReader(objects))
|
||||||
|
g.Expect(err).To(Not(HaveOccurred()))
|
||||||
|
|
||||||
|
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
|
||||||
|
for _, obj := range objs {
|
||||||
|
builder = builder.WithObjects(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventList := &corev1.EventList{}
|
||||||
|
for _, obj := range objs {
|
||||||
|
infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason")
|
||||||
|
warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason")
|
||||||
|
eventList.Items = append(eventList.Items, infoEvent, warningEvent)
|
||||||
|
}
|
||||||
|
builder = builder.WithLists(eventList)
|
||||||
|
c := builder.Build()
|
||||||
|
|
||||||
|
totalEvents := len(eventList.Items)
|
||||||
|
g.Expect(totalEvents).To(BeNumerically(">", 2), "need more than 2 events to test pagination")
|
||||||
|
|
||||||
|
// Wrap the client to paginate at 2 items per page, forcing multiple
|
||||||
|
// round-trips through FollowContinue.
|
||||||
|
pc := &paginatedClient{Client: c, pageSize: 2}
|
||||||
|
|
||||||
|
el := &corev1.EventList{}
|
||||||
|
err = addEventsToList(context.Background(), pc, el, nil)
|
||||||
|
g.Expect(err).To(Not(HaveOccurred()))
|
||||||
|
g.Expect(el.Items).To(HaveLen(totalEvents),
|
||||||
|
"addEventsToList should collect all events across paginated responses")
|
||||||
|
}
|
||||||
|
|
||||||
func kindNameIndexer(obj client.Object) []string {
|
func kindNameIndexer(obj client.Object) []string {
|
||||||
e, ok := obj.(*corev1.Event)
|
e, ok := obj.(*corev1.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
84
cmd/flux/export_source_external.go
Normal file
84
cmd/flux/export_source_external.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exportSourceExternalCmd = &cobra.Command{
|
||||||
|
Use: "external [name]",
|
||||||
|
Short: "Export ExternalArtifact sources in YAML format",
|
||||||
|
Long: "The export source external command exports one or all ExternalArtifact sources in YAML format.",
|
||||||
|
Example: ` # Export all ExternalArtifact sources
|
||||||
|
flux export source external --all > sources.yaml
|
||||||
|
|
||||||
|
# Export a specific ExternalArtifact
|
||||||
|
flux export source external my-artifact > source.yaml`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
|
||||||
|
RunE: exportWithSecretCommand{
|
||||||
|
list: externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||||
|
object: externalArtifactAdapter{&sourcev1.ExternalArtifact{}},
|
||||||
|
}.run,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
exportSourceCmd.AddCommand(exportSourceExternalCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportExternalArtifact(source *sourcev1.ExternalArtifact) any {
|
||||||
|
gvk := sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)
|
||||||
|
export := sourcev1.ExternalArtifact{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: gvk.Kind,
|
||||||
|
APIVersion: gvk.GroupVersion().String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: source.Name,
|
||||||
|
Namespace: source.Namespace,
|
||||||
|
Labels: source.Labels,
|
||||||
|
Annotations: source.Annotations,
|
||||||
|
},
|
||||||
|
Spec: source.Spec,
|
||||||
|
}
|
||||||
|
return export
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExternalArtifactSecret(source *sourcev1.ExternalArtifact) *types.NamespacedName {
|
||||||
|
// ExternalArtifact does not have a secretRef in its spec, this satisfies the interface
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex externalArtifactAdapter) secret() *types.NamespacedName {
|
||||||
|
return getExternalArtifactSecret(ex.ExternalArtifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex externalArtifactListAdapter) secretItem(i int) *types.NamespacedName {
|
||||||
|
return getExternalArtifactSecret(&ex.ExternalArtifactList.Items[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex externalArtifactAdapter) export() any {
|
||||||
|
return exportExternalArtifact(ex.ExternalArtifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex externalArtifactListAdapter) exportItem(i int) any {
|
||||||
|
return exportExternalArtifact(&ex.ExternalArtifactList.Items[i])
|
||||||
|
}
|
||||||
@@ -110,6 +110,12 @@ func TestExport(t *testing.T) {
|
|||||||
"testdata/export/bucket.yaml",
|
"testdata/export/bucket.yaml",
|
||||||
tmpl,
|
tmpl,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source external",
|
||||||
|
"export source external flux-system",
|
||||||
|
"testdata/export/external-artifact.yaml",
|
||||||
|
tmpl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range cases {
|
for _, tt := range cases {
|
||||||
|
|||||||
@@ -28,13 +28,22 @@ 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{
|
||||||
@@ -69,6 +78,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,16 +89,45 @@ 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)
|
||||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
row := 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", "Revision", "Suspended", "Ready", "Message"}
|
headers := []string{"Name"}
|
||||||
|
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...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,22 @@ 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{
|
||||||
@@ -74,6 +83,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +93,27 @@ 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)
|
||||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
row := 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", "Revision", "Suspended", "Ready", "Message"}
|
headers := []string{"Name"}
|
||||||
|
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...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ var getSourceAllCmd = &cobra.Command{
|
|||||||
apiType: helmChartType,
|
apiType: helmChartType,
|
||||||
list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
|
list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
apiType: externalArtifactType,
|
||||||
|
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range allSourceCmd {
|
for _, c := range allSourceCmd {
|
||||||
|
|||||||
108
cmd/flux/get_source_external.go
Normal file
108
cmd/flux/get_source_external.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getSourceExternalCmd = &cobra.Command{
|
||||||
|
Use: "external",
|
||||||
|
Short: "Get ExternalArtifact source statuses",
|
||||||
|
Long: `The get sources external command prints the status of the ExternalArtifact sources.`,
|
||||||
|
Example: ` # List all ExternalArtifacts and their status
|
||||||
|
flux get sources external
|
||||||
|
|
||||||
|
# List ExternalArtifacts from all namespaces
|
||||||
|
flux get sources external --all-namespaces`,
|
||||||
|
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
get := getCommand{
|
||||||
|
apiType: externalArtifactType,
|
||||||
|
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||||
|
funcMap: make(typeMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) {
|
||||||
|
o, ok := obj.(*sourcev1.ExternalArtifact)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("impossible to cast type %#v to ExternalArtifact", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
sink := &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{
|
||||||
|
Items: []sourcev1.ExternalArtifact{
|
||||||
|
*o,
|
||||||
|
}}}
|
||||||
|
return sink, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := get.run(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
getSourceCmd.AddCommand(getSourceExternalCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *externalArtifactListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||||
|
item := a.Items[i]
|
||||||
|
var revision string
|
||||||
|
if item.Status.Artifact != nil {
|
||||||
|
revision = item.Status.Artifact.Revision
|
||||||
|
}
|
||||||
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
|
revision = utils.TruncateHex(revision)
|
||||||
|
msg = utils.TruncateHex(msg)
|
||||||
|
|
||||||
|
var source string
|
||||||
|
if item.Spec.SourceRef != nil {
|
||||||
|
source = fmt.Sprintf("%s/%s/%s",
|
||||||
|
item.Spec.SourceRef.Kind,
|
||||||
|
item.Spec.SourceRef.Namespace,
|
||||||
|
item.Spec.SourceRef.Name)
|
||||||
|
}
|
||||||
|
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||||
|
revision, source, status, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactListAdapter) headers(includeNamespace bool) []string {
|
||||||
|
headers := []string{"Name", "Revision", "Source", "Ready", "Message"}
|
||||||
|
if includeNamespace {
|
||||||
|
headers = append([]string{"Namespace"}, headers...)
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||||
|
item := a.Items[i]
|
||||||
|
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ var listArtifactsCmd = &cobra.Command{
|
|||||||
Long: `The list command fetches the tags and their metadata from a remote OCI repository.
|
Long: `The list command fetches the tags and their metadata from a remote OCI repository.
|
||||||
The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`,
|
The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`,
|
||||||
Example: ` # List the artifacts stored in an OCI repository
|
Example: ` # List the artifacts stored in an OCI repository
|
||||||
flux list artifact oci://ghcr.io/org/config/app
|
flux list artifacts oci://ghcr.io/org/config/app
|
||||||
`,
|
`,
|
||||||
RunE: listArtifactsCmdRun,
|
RunE: listArtifactsCmdRun,
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func listArtifactsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
ociOpt, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String())
|
ociOpt, _, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ 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)
|
||||||
@@ -119,6 +129,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +150,8 @@ 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
|
||||||
@@ -180,12 +193,14 @@ func main() {
|
|||||||
|
|
||||||
// This is required because controller-runtime expects its consumers to
|
// This is required because controller-runtime expects its consumers to
|
||||||
// set a logger through log.SetLogger within 30 seconds of the program's
|
// set a logger through log.SetLogger within 30 seconds of the program's
|
||||||
// initalization. If not set, the entire debug stack is printed as an
|
// initialization. If not set, the entire debug stack is printed as an
|
||||||
// error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59
|
// error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59
|
||||||
// Since we have our own logging and don't care about controller-runtime's
|
// Since we have our own logging and don't care about controller-runtime's
|
||||||
// logger, we configure it's logger to do nothing.
|
// logger, we configure it's logger to do nothing.
|
||||||
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
||||||
|
|
||||||
|
registerPlugins()
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
|
||||||
if err, ok := err.(*RequestError); ok {
|
if err, ok := err.(*RequestError); ok {
|
||||||
@@ -203,6 +218,26 @@ 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")
|
||||||
@@ -225,7 +260,9 @@ func configureDefaultNamespace() {
|
|||||||
func readPasswordFromStdin(prompt string) (string, error) {
|
func readPasswordFromStdin(prompt string) (string, error) {
|
||||||
var out string
|
var out string
|
||||||
var err error
|
var err error
|
||||||
fmt.Fprint(os.Stdout, prompt)
|
if _, err := fmt.Fprint(os.Stdout, prompt); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write prompt: %w", err)
|
||||||
|
}
|
||||||
stdinFD := int(os.Stdin.Fd())
|
stdinFD := int(os.Stdin.Fd())
|
||||||
if term.IsTerminal(stdinFD) {
|
if term.IsTerminal(stdinFD) {
|
||||||
var inBytes []byte
|
var inBytes []byte
|
||||||
|
|||||||
221
cmd/flux/main_context_ns_test.go
Normal file
221
cmd/flux/main_context_ns_test.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -374,6 +374,12 @@ 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 {
|
||||||
@@ -454,7 +460,9 @@ func resetCmdArgs() {
|
|||||||
rhrArgs = reconcileHelmReleaseFlags{}
|
rhrArgs = reconcileHelmReleaseFlags{}
|
||||||
rksArgs = reconcileKsFlags{}
|
rksArgs = reconcileKsFlags{}
|
||||||
secretGitArgs = NewSecretGitFlags()
|
secretGitArgs = NewSecretGitFlags()
|
||||||
|
secretGitHubAppArgs = secretGitHubAppFlags{}
|
||||||
secretProxyArgs = secretProxyFlags{}
|
secretProxyArgs = secretProxyFlags{}
|
||||||
|
secretReceiverArgs = secretReceiverFlags{}
|
||||||
secretHelmArgs = secretHelmFlags{}
|
secretHelmArgs = secretHelmFlags{}
|
||||||
secretTLSArgs = secretTLSFlags{}
|
secretTLSArgs = secretTLSFlags{}
|
||||||
sourceBucketArgs = sourceBucketFlags{}
|
sourceBucketArgs = sourceBucketFlags{}
|
||||||
|
|||||||
@@ -18,41 +18,200 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/ssa"
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/util/retry"
|
"k8s.io/client-go/util/retry"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
|
imageautov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||||
|
imageautov1b2 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
|
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||||
|
imagev1b2 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||||
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
|
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
|
swv1b1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// APIVersions holds the mapping of GroupKinds to their respective
|
||||||
|
// latest API versions for a specific Flux version.
|
||||||
|
type APIVersions struct {
|
||||||
|
FluxVersion string
|
||||||
|
LatestVersions map[schema.GroupKind]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Update this mapping when new Flux minor versions are released!
|
||||||
|
// latestAPIVersions contains the latest API versions for each GroupKind
|
||||||
|
// for each supported Flux version. The number of latest minor versions
|
||||||
|
// we maintain here must match what's documented here:
|
||||||
|
//
|
||||||
|
// https://fluxcd.io/flux/releases/#supported-releases
|
||||||
|
var latestAPIVersions = []APIVersions{
|
||||||
|
{
|
||||||
|
FluxVersion: "2.8",
|
||||||
|
LatestVersions: flux27LatestAPIVersions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
FluxVersion: "2.7",
|
||||||
|
LatestVersions: flux27LatestAPIVersions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
FluxVersion: "2.6",
|
||||||
|
LatestVersions: flux26LatestAPIVersions,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var flux27LatestAPIVersions = map[schema.GroupKind]string{
|
||||||
|
// source-controller
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// kustomize-controller
|
||||||
|
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// helm-controller
|
||||||
|
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||||
|
|
||||||
|
// notification-controller
|
||||||
|
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-reflector-controller
|
||||||
|
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImageRepositoryKind}: imagev1.GroupVersion.Version,
|
||||||
|
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImagePolicyKind}: imagev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-automation-controller
|
||||||
|
{Group: imageautov1.GroupVersion.Group, Kind: imageautov1.ImageUpdateAutomationKind}: imageautov1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// source-watcher
|
||||||
|
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
var flux26LatestAPIVersions = map[schema.GroupKind]string{
|
||||||
|
// source-controller
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||||
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// kustomize-controller
|
||||||
|
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||||
|
|
||||||
|
// helm-controller
|
||||||
|
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||||
|
|
||||||
|
// notification-controller
|
||||||
|
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-reflector-controller
|
||||||
|
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImageRepositoryKind}: imagev1b2.GroupVersion.Version,
|
||||||
|
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImagePolicyKind}: imagev1b2.GroupVersion.Version,
|
||||||
|
|
||||||
|
// image-automation-controller
|
||||||
|
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
||||||
|
}
|
||||||
|
|
||||||
var migrateCmd = &cobra.Command{
|
var migrateCmd = &cobra.Command{
|
||||||
Use: "migrate",
|
Use: "migrate",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Short: "Migrate the Flux custom resources to their latest API version",
|
Short: "Migrate the Flux custom resources to their latest API version",
|
||||||
Long: `The migrate command must be run before a Flux minor version upgrade.
|
Long: `The migrate command must be run before a Flux minor version upgrade.
|
||||||
The command migrates the Flux custom resources stored in Kubernetes etcd to their latest API version,
|
|
||||||
ensuring the Flux components can continue to function correctly after the upgrade.
|
The command has two modes of operation:
|
||||||
|
|
||||||
|
- Cluster mode (default): migrates all the Flux custom resources stored in Kubernetes etcd to their latest API version.
|
||||||
|
- File system mode (-f): migrates the Flux custom resources defined in the manifests located in the specified path.
|
||||||
|
`,
|
||||||
|
Example: ` # Migrate all the Flux custom resources in the cluster.
|
||||||
|
# This uses the current kubeconfig context and requires cluster-admin permissions.
|
||||||
|
flux migrate
|
||||||
|
|
||||||
|
# Migrate all the Flux custom resources in a Git repository
|
||||||
|
# checked out in the current working directory.
|
||||||
|
flux migrate -f .
|
||||||
|
|
||||||
|
# Migrate all Flux custom resources defined in YAML and Helm YAML template files.
|
||||||
|
flux migrate -f . --extensions=.yml,.yaml,.tpl
|
||||||
|
|
||||||
|
# Migrate the Flux custom resources to the latest API versions of Flux 2.6.
|
||||||
|
flux migrate -f . --version=2.6
|
||||||
|
|
||||||
|
# Migrate the Flux custom resources defined in a multi-document YAML manifest file.
|
||||||
|
flux migrate -f path/to/manifest.yaml
|
||||||
|
|
||||||
|
# Simulate the migration without making any changes.
|
||||||
|
flux migrate -f . --dry-run
|
||||||
|
|
||||||
|
# Run the migration skipping confirmation prompts.
|
||||||
|
flux migrate -f . --yes
|
||||||
`,
|
`,
|
||||||
RunE: runMigrateCmd,
|
RunE: runMigrateCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
var migrateFlags struct {
|
||||||
rootCmd.AddCommand(migrateCmd)
|
yes bool
|
||||||
|
dryRun bool
|
||||||
|
path string
|
||||||
|
version string
|
||||||
|
extensions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMigrateCmd(cmd *cobra.Command, args []string) error {
|
func init() {
|
||||||
|
rootCmd.AddCommand(migrateCmd)
|
||||||
|
|
||||||
|
migrateCmd.Flags().StringVarP(&migrateFlags.path, "path", "f", "",
|
||||||
|
"the path to the directory containing the manifests to migrate")
|
||||||
|
migrateCmd.Flags().StringSliceVarP(&migrateFlags.extensions, "extensions", "e", []string{".yaml", ".yml"},
|
||||||
|
"the file extensions to consider when migrating manifests, only applicable with --path")
|
||||||
|
migrateCmd.Flags().StringVarP(&migrateFlags.version, "version", "v", "",
|
||||||
|
"the target Flux minor version to migrate manifests to, only applicable with --path (defaults to the version of the CLI)")
|
||||||
|
migrateCmd.Flags().BoolVarP(&migrateFlags.yes, "yes", "y", false,
|
||||||
|
"skip confirmation prompts when migrating manifests, only applicable with --path")
|
||||||
|
migrateCmd.Flags().BoolVar(&migrateFlags.dryRun, "dry-run", false,
|
||||||
|
"simulate the migration of manifests without making any changes, only applicable with --path")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrateCmd(*cobra.Command, []string) error {
|
||||||
|
if migrateFlags.path == "" {
|
||||||
|
return migrateCluster()
|
||||||
|
}
|
||||||
|
return migrateFileSystem()
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateCluster() error {
|
||||||
logger.Actionf("starting migration of custom resources")
|
logger.Actionf("starting migration of custom resources")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
|
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Kubernetes client initialization failed: %s", err.Error())
|
return fmt.Errorf("the Kubernetes client initialization failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()})
|
kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()})
|
||||||
@@ -60,7 +219,7 @@ func runMigrateCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
migrator := NewMigrator(kubeClient, client.MatchingLabels{
|
migrator := NewClusterMigrator(kubeClient, client.MatchingLabels{
|
||||||
"app.kubernetes.io/part-of": "flux",
|
"app.kubernetes.io/part-of": "flux",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -72,28 +231,64 @@ func runMigrateCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Migrator struct {
|
func migrateFileSystem() error {
|
||||||
|
pathRoot, err := os.OpenRoot(".")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open filesystem at the current working directory: %w", err)
|
||||||
|
}
|
||||||
|
defer pathRoot.Close()
|
||||||
|
|
||||||
|
fileSystem := &osFS{pathRoot.FS()}
|
||||||
|
yes := migrateFlags.yes
|
||||||
|
dryRun := migrateFlags.dryRun
|
||||||
|
path := migrateFlags.path
|
||||||
|
extensions := migrateFlags.extensions
|
||||||
|
var latestVersions map[schema.GroupKind]string
|
||||||
|
|
||||||
|
// Determine latest API versions based on the Flux version.
|
||||||
|
if migrateFlags.version == "" {
|
||||||
|
latestVersions = latestAPIVersions[0].LatestVersions
|
||||||
|
} else {
|
||||||
|
supportedVersions := make([]string, 0, len(latestAPIVersions))
|
||||||
|
for _, v := range latestAPIVersions {
|
||||||
|
if v.FluxVersion == migrateFlags.version {
|
||||||
|
latestVersions = v.LatestVersions
|
||||||
|
break
|
||||||
|
}
|
||||||
|
supportedVersions = append(supportedVersions, v.FluxVersion)
|
||||||
|
}
|
||||||
|
if latestVersions == nil {
|
||||||
|
return fmt.Errorf("version %s is not supported, supported versions are: %s",
|
||||||
|
migrateFlags.version, strings.Join(supportedVersions, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFileSystemMigrator(fileSystem, yes, dryRun, path, extensions, latestVersions).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterMigrator migrates all the CRs in the cluster for the CRDs matching the label selector.
|
||||||
|
type ClusterMigrator struct {
|
||||||
labelSelector client.MatchingLabels
|
labelSelector client.MatchingLabels
|
||||||
kubeClient client.Client
|
kubeClient client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMigrator creates a new Migrator instance with the specified label selector.
|
// NewClusterMigrator creates a new ClusterMigrator instance with the specified label selector.
|
||||||
func NewMigrator(kubeClient client.Client, labelSelector client.MatchingLabels) *Migrator {
|
func NewClusterMigrator(kubeClient client.Client, labelSelector client.MatchingLabels) *ClusterMigrator {
|
||||||
return &Migrator{
|
return &ClusterMigrator{
|
||||||
labelSelector: labelSelector,
|
labelSelector: labelSelector,
|
||||||
kubeClient: kubeClient,
|
kubeClient: kubeClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migrator) Run(ctx context.Context) error {
|
func (c *ClusterMigrator) Run(ctx context.Context) error {
|
||||||
crdList := &apiextensionsv1.CustomResourceDefinitionList{}
|
crdList := &apiextensionsv1.CustomResourceDefinitionList{}
|
||||||
|
|
||||||
if err := m.kubeClient.List(ctx, crdList, m.labelSelector); err != nil {
|
if err := c.kubeClient.List(ctx, crdList, c.labelSelector); err != nil {
|
||||||
return fmt.Errorf("failed to list CRDs: %w", err)
|
return fmt.Errorf("failed to list CRDs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, crd := range crdList.Items {
|
for _, crd := range crdList.Items {
|
||||||
if err := m.migrateCRD(ctx, crd.Name); err != nil {
|
if err := c.migrateCRD(ctx, crd.Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,22 +296,22 @@ func (m *Migrator) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migrator) migrateCRD(ctx context.Context, name string) error {
|
func (c *ClusterMigrator) migrateCRD(ctx context.Context, name string) error {
|
||||||
crd := &apiextensionsv1.CustomResourceDefinition{}
|
crd := &apiextensionsv1.CustomResourceDefinition{}
|
||||||
|
|
||||||
if err := m.kubeClient.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil {
|
if err := c.kubeClient.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil {
|
||||||
return fmt.Errorf("failed to get CRD %s: %w", name, err)
|
return fmt.Errorf("failed to get CRD %s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the latest storage version for the CRD
|
// get the latest storage version for the CRD
|
||||||
storageVersion := m.getStorageVersion(crd)
|
storageVersion := c.getStorageVersion(crd)
|
||||||
if storageVersion == "" {
|
if storageVersion == "" {
|
||||||
return fmt.Errorf("no storage version found for CRD %s", name)
|
return fmt.Errorf("no storage version found for CRD %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrate all the resources for the CRD
|
// migrate all the resources for the CRD
|
||||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||||
return m.migrateCR(ctx, crd, storageVersion)
|
return c.migrateCR(ctx, crd, storageVersion)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to migrate resources for CRD %s: %w", name, err)
|
return fmt.Errorf("failed to migrate resources for CRD %s: %w", name, err)
|
||||||
@@ -125,7 +320,7 @@ func (m *Migrator) migrateCRD(ctx context.Context, name string) error {
|
|||||||
// set the CRD status to contain only the latest storage version
|
// set the CRD status to contain only the latest storage version
|
||||||
if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != storageVersion {
|
if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != storageVersion {
|
||||||
crd.Status.StoredVersions = []string{storageVersion}
|
crd.Status.StoredVersions = []string{storageVersion}
|
||||||
if err := m.kubeClient.Status().Update(ctx, crd); err != nil {
|
if err := c.kubeClient.Status().Update(ctx, crd); err != nil {
|
||||||
return fmt.Errorf("failed to update CRD %s status: %w", crd.Name, err)
|
return fmt.Errorf("failed to update CRD %s status: %w", crd.Name, err)
|
||||||
}
|
}
|
||||||
logger.Successf("%s migrated to storage version %s", crd.Name, storageVersion)
|
logger.Successf("%s migrated to storage version %s", crd.Name, storageVersion)
|
||||||
@@ -133,8 +328,8 @@ func (m *Migrator) migrateCRD(ctx context.Context, name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrateCR migrates all CRs for the given CRD to the specified version by patching them with an empty patch.
|
// migrateCR migrates all CRs for the given CRD to the specified version by patching them.
|
||||||
func (m *Migrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, version string) error {
|
func (c *ClusterMigrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, version string) error {
|
||||||
list := &unstructured.UnstructuredList{}
|
list := &unstructured.UnstructuredList{}
|
||||||
|
|
||||||
apiVersion := crd.Spec.Group + "/" + version
|
apiVersion := crd.Spec.Group + "/" + version
|
||||||
@@ -143,7 +338,7 @@ func (m *Migrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomRes
|
|||||||
list.SetAPIVersion(apiVersion)
|
list.SetAPIVersion(apiVersion)
|
||||||
list.SetKind(listKind)
|
list.SetKind(listKind)
|
||||||
|
|
||||||
err := m.kubeClient.List(ctx, list, client.InNamespace(""))
|
err := c.kubeClient.List(ctx, list, client.InNamespace(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list resources for CRD %s: %w", crd.Name, err)
|
return fmt.Errorf("failed to list resources for CRD %s: %w", crd.Name, err)
|
||||||
}
|
}
|
||||||
@@ -153,8 +348,15 @@ func (m *Migrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range list.Items {
|
for _, item := range list.Items {
|
||||||
|
patches, err := ssa.PatchMigrateToVersion(&item, apiVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration patch for %s/%s/%s: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(patches) == 0 {
|
||||||
// patch the resource with an empty patch to update the version
|
// patch the resource with an empty patch to update the version
|
||||||
if err := m.kubeClient.Patch(
|
if err := c.kubeClient.Patch(
|
||||||
ctx,
|
ctx,
|
||||||
&item,
|
&item,
|
||||||
client.RawPatch(client.Merge.Type(), []byte("{}")),
|
client.RawPatch(client.Merge.Type(), []byte("{}")),
|
||||||
@@ -162,6 +364,22 @@ func (m *Migrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomRes
|
|||||||
return fmt.Errorf(" %s/%s/%s failed to migrate: %w",
|
return fmt.Errorf(" %s/%s/%s failed to migrate: %w",
|
||||||
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// patch the resource to migrate the managed fields to the latest apiVersion
|
||||||
|
rawPatch, err := json.Marshal(patches)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal migration patch for %s/%s/%s: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
if err := c.kubeClient.Patch(
|
||||||
|
ctx,
|
||||||
|
&item,
|
||||||
|
client.RawPatch(types.JSONPatchType, rawPatch),
|
||||||
|
); err != nil && !apierrors.IsNotFound(err) {
|
||||||
|
return fmt.Errorf(" %s/%s/%s failed to migrate managed fields: %w",
|
||||||
|
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Successf("%s/%s/%s migrated to version %s",
|
logger.Successf("%s/%s/%s migrated to version %s",
|
||||||
item.GetKind(), item.GetNamespace(), item.GetName(), version)
|
item.GetKind(), item.GetNamespace(), item.GetName(), version)
|
||||||
@@ -171,7 +389,7 @@ func (m *Migrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getStorageVersion retrieves the storage version of a CustomResourceDefinition.
|
// getStorageVersion retrieves the storage version of a CustomResourceDefinition.
|
||||||
func (m *Migrator) getStorageVersion(crd *apiextensionsv1.CustomResourceDefinition) string {
|
func (c *ClusterMigrator) getStorageVersion(crd *apiextensionsv1.CustomResourceDefinition) string {
|
||||||
var version string
|
var version string
|
||||||
for _, v := range crd.Spec.Versions {
|
for _, v := range crd.Spec.Versions {
|
||||||
if v.Storage {
|
if v.Storage {
|
||||||
@@ -182,3 +400,303 @@ func (m *Migrator) getStorageVersion(crd *apiextensionsv1.CustomResourceDefiniti
|
|||||||
|
|
||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WritableFS extends fs.FS with a WriteFile method.
|
||||||
|
type WritableFS interface {
|
||||||
|
fs.FS
|
||||||
|
WriteFile(name string, data []byte, perm os.FileMode) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// osFS is a WritableFS implementation that uses the file system of the OS.
|
||||||
|
type osFS struct {
|
||||||
|
fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *osFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
return os.WriteFile(name, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSystemMigrator migrates all the CRs found in the manifests located in the specified path.
|
||||||
|
type FileSystemMigrator struct {
|
||||||
|
fileSystem WritableFS
|
||||||
|
yes bool
|
||||||
|
dryRun bool
|
||||||
|
path string
|
||||||
|
extensions []string
|
||||||
|
latestVersions map[schema.GroupKind]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileAPIUpgrades represents the API upgrades detected in a specific manifest file.
|
||||||
|
type FileAPIUpgrades struct {
|
||||||
|
File string
|
||||||
|
Upgrades []APIUpgrade
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIUpgrade represents an upgrade of a specific API version in a manifest file.
|
||||||
|
type APIUpgrade struct {
|
||||||
|
Line int
|
||||||
|
Kind string
|
||||||
|
OldVersion string
|
||||||
|
NewVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileSystemMigrator creates a new FileSystemMigrator instance with the specified flags.
|
||||||
|
func NewFileSystemMigrator(fileSystem WritableFS, yes, dryRun bool, path string,
|
||||||
|
extensions []string, latestVersions map[schema.GroupKind]string) *FileSystemMigrator {
|
||||||
|
return &FileSystemMigrator{
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
yes: yes,
|
||||||
|
dryRun: dryRun,
|
||||||
|
path: filepath.Clean(path), // convert dir/ to dir to avoid error when walking
|
||||||
|
extensions: extensions,
|
||||||
|
latestVersions: latestVersions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) Run() error {
|
||||||
|
logger.Actionf("starting migration of custom resources")
|
||||||
|
|
||||||
|
// List and filter files.
|
||||||
|
files, err := f.listFiles()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect upgrades.
|
||||||
|
upgrades, err := f.detectUpgrades(files)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(upgrades) == 0 {
|
||||||
|
logger.Successf("no custom resources found that require migration")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if f.dryRun {
|
||||||
|
logger.Successf("dry-run mode enabled, no changes will be made")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm upgrades.
|
||||||
|
if !f.yes {
|
||||||
|
prompt := promptui.Prompt{
|
||||||
|
Label: "Are you sure you want to proceed with the above upgrades", // Already prints "? [y/N]"
|
||||||
|
IsConfirm: true,
|
||||||
|
}
|
||||||
|
if _, err := prompt.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate files.
|
||||||
|
for _, fileUpgrades := range upgrades {
|
||||||
|
if err := f.migrateFile(&fileUpgrades); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Successf("file %s migrated successfully", fileUpgrades.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Successf("custom resources migrated successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) listFiles() ([]string, error) {
|
||||||
|
fileInfo, err := fs.Stat(f.fileSystem, f.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to stat path %s: %w", f.path, err)
|
||||||
|
}
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
return f.listDirectoryFiles()
|
||||||
|
}
|
||||||
|
if err := f.validateSingleFile(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []string{f.path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) listDirectoryFiles() ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
err := fs.WalkDir(f.fileSystem, f.path, func(path string, dirEntry fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !f.matchesExtensions(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fileInfo, err := dirEntry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fileInfo.Mode().IsRegular() {
|
||||||
|
files = append(files, path)
|
||||||
|
} else if !fileInfo.IsDir() {
|
||||||
|
logger.Warningf("skipping irregular file %s", path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to walk directory %s: %w", f.path, err)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) validateSingleFile() error {
|
||||||
|
if !f.matchesExtensions(f.path) {
|
||||||
|
return fmt.Errorf("file %s does not match the specified extensions: %v",
|
||||||
|
f.path, strings.Join(f.extensions, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's irregular by walking the parent directory.
|
||||||
|
var irregular bool
|
||||||
|
err := fs.WalkDir(f.fileSystem, filepath.Dir(f.path), func(path string, dirEntry fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if path != f.path {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fileInfo, err := dirEntry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !fileInfo.Mode().IsRegular() {
|
||||||
|
irregular = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to validate file %s: %w", f.path, err)
|
||||||
|
}
|
||||||
|
if irregular {
|
||||||
|
return fmt.Errorf("file %s is irregular", f.path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) matchesExtensions(file string) bool {
|
||||||
|
for _, ext := range f.extensions {
|
||||||
|
if strings.HasSuffix(file, ext) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) detectUpgrades(files []string) ([]FileAPIUpgrades, error) {
|
||||||
|
var upgrades []FileAPIUpgrades
|
||||||
|
for _, file := range files {
|
||||||
|
fileUpgrades, err := f.detectFileUpgrades(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(fileUpgrades) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fu := FileAPIUpgrades{
|
||||||
|
File: file,
|
||||||
|
Upgrades: fileUpgrades,
|
||||||
|
}
|
||||||
|
upgrades = append(upgrades, fu)
|
||||||
|
f.printDetectedUpgrades(&fu)
|
||||||
|
}
|
||||||
|
return upgrades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) detectFileUpgrades(file string) ([]APIUpgrade, error) {
|
||||||
|
b, err := fs.ReadFile(f.fileSystem, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read file %s: %w", file, err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(b), "\n")
|
||||||
|
|
||||||
|
var fileUpgrades []APIUpgrade
|
||||||
|
for line, apiVersionLine := range lines {
|
||||||
|
// Parse apiVersion.
|
||||||
|
const apiVersionPrefix = "apiVersion: "
|
||||||
|
idx := strings.Index(apiVersionLine, apiVersionPrefix)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiVersionValuePrefix := strings.TrimSpace(apiVersionLine[idx+len(apiVersionPrefix):])
|
||||||
|
apiVersion := strings.Split(apiVersionValuePrefix, " ")[0]
|
||||||
|
gv, err := schema.ParseGroupVersion(apiVersion)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("%s:%d: %v", file, line+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse kind.
|
||||||
|
if line+1 >= len(lines) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kindLine := lines[line+1]
|
||||||
|
const kindPrefix = "kind: "
|
||||||
|
idx = strings.Index(kindLine, kindPrefix)
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kindValuePrefix := strings.TrimSpace(kindLine[idx+len(kindPrefix):])
|
||||||
|
kind := strings.Split(kindValuePrefix, " ")[0]
|
||||||
|
|
||||||
|
// Build GroupKind.
|
||||||
|
gk := schema.GroupKind{
|
||||||
|
Group: gv.Group,
|
||||||
|
Kind: kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a newer version for the GroupKind.
|
||||||
|
latestVersion, ok := f.latestVersions[gk]
|
||||||
|
if !ok || latestVersion == gv.Version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the upgrade.
|
||||||
|
fileUpgrades = append(fileUpgrades, APIUpgrade{
|
||||||
|
Line: line,
|
||||||
|
Kind: kind,
|
||||||
|
OldVersion: gv.Version,
|
||||||
|
NewVersion: latestVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fileUpgrades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) printDetectedUpgrades(fileUpgrades *FileAPIUpgrades) {
|
||||||
|
for _, upgrade := range fileUpgrades.Upgrades {
|
||||||
|
logger.Generatef("%s:%d: %s %s -> %s",
|
||||||
|
fileUpgrades.File,
|
||||||
|
upgrade.Line+1,
|
||||||
|
upgrade.Kind,
|
||||||
|
upgrade.OldVersion,
|
||||||
|
upgrade.NewVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileSystemMigrator) migrateFile(fileUpgrades *FileAPIUpgrades) error {
|
||||||
|
// Read file and map lines.
|
||||||
|
b, err := fs.ReadFile(f.fileSystem, fileUpgrades.File)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file %s: %w", fileUpgrades.File, err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(b), "\n")
|
||||||
|
|
||||||
|
// Apply upgrades to lines.
|
||||||
|
for _, upgrade := range fileUpgrades.Upgrades {
|
||||||
|
line := lines[upgrade.Line]
|
||||||
|
line = strings.Replace(line, upgrade.OldVersion, upgrade.NewVersion, 1)
|
||||||
|
lines[upgrade.Line] = line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file info to preserve permissions.
|
||||||
|
fileInfo, err := fs.Stat(f.fileSystem, fileUpgrades.File)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat file %s: %w", fileUpgrades.File, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file with preserved permissions.
|
||||||
|
b = []byte(strings.Join(lines, "\n"))
|
||||||
|
if err := f.fileSystem.WriteFile(fileUpgrades.File, b, fileInfo.Mode()); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %s: %w", fileUpgrades.File, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
161
cmd/flux/migrate_test.go
Normal file
161
cmd/flux/migrate_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type writeToMemoryFS struct {
|
||||||
|
fs.FS
|
||||||
|
|
||||||
|
writtenFiles map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *writeToMemoryFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||||
|
m.writtenFiles[name] = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type writtenFile struct {
|
||||||
|
file string
|
||||||
|
goldenFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSystemMigrator(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
outputGolden string
|
||||||
|
writtenFiles []writtenFile
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "errors out for single file that is a symlink",
|
||||||
|
path: "testdata/migrate/file-system/single-file-link.yaml",
|
||||||
|
err: "file testdata/migrate/file-system/single-file-link.yaml is irregular",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "errors out for single file with wrong extension",
|
||||||
|
path: "testdata/migrate/file-system/single-file-wrong-ext.json",
|
||||||
|
err: "file testdata/migrate/file-system/single-file-wrong-ext.json does not match the specified extensions: .yaml, .yml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrate single file",
|
||||||
|
path: "testdata/migrate/file-system/single-file.yaml",
|
||||||
|
outputGolden: "testdata/migrate/file-system/single-file.yaml.output.golden",
|
||||||
|
writtenFiles: []writtenFile{
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/single-file.yaml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/single-file.yaml.golden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "migrate files in directory",
|
||||||
|
path: "testdata/migrate/file-system/dir",
|
||||||
|
outputGolden: "testdata/migrate/file-system/dir.output.golden",
|
||||||
|
writtenFiles: []writtenFile{
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-dir/another-file.yaml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-dir/another-file.yml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-file.yaml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "testdata/migrate/file-system/dir/some-file.yml",
|
||||||
|
goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Store logger, replace with test logger, and restore at the end of the test.
|
||||||
|
var testLogger bytes.Buffer
|
||||||
|
oldLogger := logger
|
||||||
|
logger = stderrLogger{&testLogger}
|
||||||
|
t.Cleanup(func() { logger = oldLogger })
|
||||||
|
|
||||||
|
// Open current working directory as root and build write-to-memory filesystem.
|
||||||
|
pathRoot, err := os.OpenRoot(".")
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
t.Cleanup(func() { pathRoot.Close() })
|
||||||
|
fileSystem := &writeToMemoryFS{
|
||||||
|
FS: pathRoot.FS(),
|
||||||
|
writtenFiles: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare other inputs.
|
||||||
|
const yes = true
|
||||||
|
const dryRun = false
|
||||||
|
extensions := []string{".yaml", ".yml"}
|
||||||
|
latestVersions := map[schema.GroupKind]string{
|
||||||
|
{Group: "image.toolkit.fluxcd.io", Kind: "ImageRepository"}: "v1",
|
||||||
|
{Group: "image.toolkit.fluxcd.io", Kind: "ImagePolicy"}: "v1",
|
||||||
|
{Group: "image.toolkit.fluxcd.io", Kind: "ImageUpdateAutomation"}: "v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration.
|
||||||
|
err = NewFileSystemMigrator(fileSystem, yes, dryRun, tt.path, extensions, latestVersions).Run()
|
||||||
|
if tt.err != "" {
|
||||||
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
g.Expect(err.Error()).To(Equal(tt.err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Assert logger output.
|
||||||
|
b, err := os.ReadFile(tt.outputGolden)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(string(b)).To(Equal(testLogger.String()),
|
||||||
|
"logger output does not match golden file %s", tt.outputGolden)
|
||||||
|
|
||||||
|
// Assert which files were written.
|
||||||
|
writtenFiles := make([]string, 0, len(fileSystem.writtenFiles))
|
||||||
|
for name := range fileSystem.writtenFiles {
|
||||||
|
writtenFiles = append(writtenFiles, name)
|
||||||
|
}
|
||||||
|
expectedWrittenFiles := make([]string, 0, len(tt.writtenFiles))
|
||||||
|
for _, wf := range tt.writtenFiles {
|
||||||
|
expectedWrittenFiles = append(expectedWrittenFiles, wf.file)
|
||||||
|
}
|
||||||
|
g.Expect(writtenFiles).To(ConsistOf(expectedWrittenFiles))
|
||||||
|
|
||||||
|
// Assert contents of written files.
|
||||||
|
for _, wf := range tt.writtenFiles {
|
||||||
|
b, err := os.ReadFile(wf.goldenFile)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(string(fileSystem.writtenFiles[wf.file])).To(Equal(string(b)),
|
||||||
|
"file %s does not match golden file %s", wf.file, wf.goldenFile)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/crane"
|
"github.com/google/go-containerregistry/pkg/crane"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/auth"
|
"github.com/fluxcd/pkg/auth"
|
||||||
@@ -28,14 +29,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// loginWithProvider gets a crane authentication option for the given provider and URL.
|
// loginWithProvider gets a crane authentication option for the given provider and URL.
|
||||||
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, error) {
|
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, authn.Authenticator, error) {
|
||||||
var opts []auth.Option
|
var opts []auth.Option
|
||||||
if provider == azure.ProviderName {
|
if provider == azure.ProviderName {
|
||||||
opts = append(opts, auth.WithAllowShellOut())
|
opts = append(opts, auth.WithAllowShellOut())
|
||||||
}
|
}
|
||||||
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...)
|
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err)
|
return nil, nil, fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err)
|
||||||
}
|
}
|
||||||
return crane.WithAuth(authenticator), nil
|
return crane.WithAuth(authenticator), authenticator, nil
|
||||||
}
|
}
|
||||||
|
|||||||
118
cmd/flux/plugin.go
Normal file
118
cmd/flux/plugin.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/briandowns/spinner"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginHandler = plugin.NewHandler()
|
||||||
|
|
||||||
|
var pluginCmd = &cobra.Command{
|
||||||
|
Use: "plugin",
|
||||||
|
Short: "Manage Flux CLI plugins",
|
||||||
|
Long: `The plugin sub-commands manage Flux CLI plugins.`,
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// No-op: skip root's namespace DNS validation for plugin commands.
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(pluginCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// builtinCommandNames returns the names of all non-plugin commands on rootCmd.
|
||||||
|
func builtinCommandNames() []string {
|
||||||
|
var names []string
|
||||||
|
for _, c := range rootCmd.Commands() {
|
||||||
|
if c.GroupID != "plugin" {
|
||||||
|
names = append(names, c.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerPlugins scans the plugin directory and registers discovered
|
||||||
|
// plugins as Cobra subcommands on rootCmd.
|
||||||
|
func registerPlugins() {
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rootCmd.ContainsGroup("plugin") {
|
||||||
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
|
ID: "plugin",
|
||||||
|
Title: "Plugin Commands:",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range plugins {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: p.Name,
|
||||||
|
Short: fmt.Sprintf("Runs the %s plugin", p.Name),
|
||||||
|
Long: fmt.Sprintf("This command runs the %s plugin.\nUse 'flux %s --help' for full plugin help.", p.Name, p.Name),
|
||||||
|
DisableFlagParsing: true,
|
||||||
|
GroupID: "plugin",
|
||||||
|
ValidArgsFunction: plugin.CompleteFunc(p.Path),
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return plugin.Exec(p.Path, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNameVersion splits "operator@0.45.0" into ("operator", "0.45.0").
|
||||||
|
// If no @ is present, version is empty (latest).
|
||||||
|
func parseNameVersion(s string) (string, string) {
|
||||||
|
name, version, found := strings.Cut(s, "@")
|
||||||
|
if found {
|
||||||
|
return name, version
|
||||||
|
}
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDigestRef reports whether ref is a content-addressable digest
|
||||||
|
// (e.g. "sha256:06e0a38...").
|
||||||
|
func isDigestRef(ref string) bool {
|
||||||
|
return strings.HasPrefix(ref, "sha256:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCatalogClient creates a CatalogClient that respects FLUXCD_PLUGIN_CATALOG.
|
||||||
|
func newCatalogClient() *plugin.CatalogClient {
|
||||||
|
client := plugin.NewCatalogClient()
|
||||||
|
client.GetEnv = pluginHandler.GetEnv
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPluginSpinner(message string) *spinner.Spinner {
|
||||||
|
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||||
|
s.Suffix = " " + message
|
||||||
|
return s
|
||||||
|
}
|
||||||
96
cmd/flux/plugin_install.go
Normal file
96
cmd/flux/plugin_install.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginInstallCmd = &cobra.Command{
|
||||||
|
Use: "install <name>[@<version>|@<digest>]",
|
||||||
|
Short: "Install a plugin from the catalog",
|
||||||
|
Long: `The plugin install command downloads and installs a plugin from the Flux plugin catalog.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Install the latest version
|
||||||
|
flux plugin install operator
|
||||||
|
|
||||||
|
# Install a specific version
|
||||||
|
flux plugin install operator@0.45.0
|
||||||
|
|
||||||
|
# Install pinned to a specific digest
|
||||||
|
flux plugin install operator@sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: pluginInstallCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginInstallCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginInstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
nameVersion := args[0]
|
||||||
|
name, ref := parseNameVersion(nameVersion)
|
||||||
|
|
||||||
|
catalogClient := newCatalogClient()
|
||||||
|
manifest, err := catalogClient.FetchManifest(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pv *plugintypes.Version
|
||||||
|
var plat *plugintypes.Platform
|
||||||
|
|
||||||
|
if isDigestRef(ref) {
|
||||||
|
dm, err := plugin.ResolveByDigest(manifest, ref, runtime.GOOS, runtime.GOARCH)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pv = dm.Version
|
||||||
|
plat = dm.Platform
|
||||||
|
} else {
|
||||||
|
pv, err = plugin.ResolveVersion(manifest, ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plat, err = plugin.ResolvePlatform(pv, runtime.GOOS, runtime.GOARCH)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plugin %q v%s has no binary for %s/%s", name, pv.Version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := pluginHandler.EnsurePluginDir()
|
||||||
|
|
||||||
|
installer := plugin.NewInstaller()
|
||||||
|
sp := newPluginSpinner(fmt.Sprintf("installing %s v%s", name, pv.Version))
|
||||||
|
sp.Start()
|
||||||
|
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||||
|
sp.Stop()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sp.Stop()
|
||||||
|
|
||||||
|
logger.Successf("installed %s v%s", name, pv.Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
57
cmd/flux/plugin_list.go
Normal file
57
cmd/flux/plugin_list.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List installed plugins",
|
||||||
|
Long: `The plugin list command shows all installed plugins with their versions and paths.`,
|
||||||
|
RunE: pluginListCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginListCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginListCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
pluginDir := pluginHandler.PluginDir()
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
cmd.Println("No plugins found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
header := []string{"NAME", "VERSION", "PATH"}
|
||||||
|
var rows [][]string
|
||||||
|
for _, p := range plugins {
|
||||||
|
version := "manual"
|
||||||
|
if receipt := plugin.ReadReceipt(pluginDir, p.Name); receipt != nil {
|
||||||
|
version = receipt.Version
|
||||||
|
}
|
||||||
|
rows = append(rows, []string{p.Name, version, p.Path})
|
||||||
|
}
|
||||||
|
|
||||||
|
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||||
|
}
|
||||||
81
cmd/flux/plugin_search.go
Normal file
81
cmd/flux/plugin_search.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginSearchCmd = &cobra.Command{
|
||||||
|
Use: "search [query]",
|
||||||
|
Short: "Search the plugin catalog",
|
||||||
|
Long: `The plugin search command lists available plugins from the Flux plugin catalog.`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: pluginSearchCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginSearchCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginSearchCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
catalogClient := newCatalogClient()
|
||||||
|
catalog, err := catalogClient.FetchCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var query string
|
||||||
|
if len(args) == 1 {
|
||||||
|
query = strings.ToLower(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := pluginHandler.PluginDir()
|
||||||
|
header := []string{"NAME", "DESCRIPTION", "INSTALLED"}
|
||||||
|
var rows [][]string
|
||||||
|
for _, entry := range catalog.Plugins {
|
||||||
|
if query != "" {
|
||||||
|
if !strings.Contains(strings.ToLower(entry.Name), query) &&
|
||||||
|
!strings.Contains(strings.ToLower(entry.Description), query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installed := ""
|
||||||
|
if receipt := plugin.ReadReceipt(pluginDir, entry.Name); receipt != nil {
|
||||||
|
installed = receipt.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{entry.Name, entry.Description, installed})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
if query != "" {
|
||||||
|
cmd.Printf("No plugins matching %q found in catalog\n", query)
|
||||||
|
} else {
|
||||||
|
cmd.Println("No plugins found in catalog")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||||
|
}
|
||||||
286
cmd/flux/plugin_test.go
Normal file
286
cmd/flux/plugin_test.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPluginAppearsInHelp(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
fakeBin := pluginDir + "/flux-testplugin"
|
||||||
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPlugins()
|
||||||
|
defer func() {
|
||||||
|
cmds := rootCmd.Commands()
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd.Name() == "testplugin" {
|
||||||
|
rootCmd.RemoveCommand(cmd)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
output, err := executeCommand("--help")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "Plugin Commands:") {
|
||||||
|
t.Error("expected 'Plugin Commands:' in help output")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "testplugin") {
|
||||||
|
t.Error("expected 'testplugin' in help output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListOutput(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
fakeBin := pluginDir + "/flux-myplugin"
|
||||||
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "myplugin") {
|
||||||
|
t.Errorf("expected 'myplugin' in output, got: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "manual") {
|
||||||
|
t.Errorf("expected 'manual' in output (no receipt), got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListWithReceipt(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
fakeBin := pluginDir + "/flux-myplugin"
|
||||||
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||||
|
receipt := pluginDir + "/flux-myplugin.yaml"
|
||||||
|
os.WriteFile(receipt, []byte("name: myplugin\nversion: \"1.2.3\"\n"), 0o644)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "1.2.3") {
|
||||||
|
t.Errorf("expected version '1.2.3' in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListEmpty(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "No plugins found") {
|
||||||
|
t.Errorf("expected 'No plugins found', got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoPluginsNoRegistration(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: func(name string) ([]os.DirEntry, error) {
|
||||||
|
return nil, fmt.Errorf("no dir")
|
||||||
|
},
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return "/nonexistent"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that registerPlugins with no plugins doesn't add any commands.
|
||||||
|
before := len(rootCmd.Commands())
|
||||||
|
registerPlugins()
|
||||||
|
after := len(rootCmd.Commands())
|
||||||
|
if after != before {
|
||||||
|
t.Errorf("expected no new commands, got %d new", after-before)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginSkipsPersistentPreRun(t *testing.T) {
|
||||||
|
// Plugin commands override root's PersistentPreRunE with a no-op,
|
||||||
|
// so an invalid namespace should not trigger a validation error.
|
||||||
|
_, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("plugin list should not trigger root's namespace validation: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseNameVersion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantName string
|
||||||
|
wantVersion string
|
||||||
|
}{
|
||||||
|
{"operator", "operator", ""},
|
||||||
|
{"operator@0.45.0", "operator", "0.45.0"},
|
||||||
|
{"my-tool@1.0.0", "my-tool", "1.0.0"},
|
||||||
|
{"plugin@", "plugin", ""},
|
||||||
|
{"operator@sha256:abc123", "operator", "sha256:abc123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
name, version := parseNameVersion(tt.input)
|
||||||
|
if name != tt.wantName {
|
||||||
|
t.Errorf("name: got %q, want %q", name, tt.wantName)
|
||||||
|
}
|
||||||
|
if version != tt.wantVersion {
|
||||||
|
t.Errorf("version: got %q, want %q", version, tt.wantVersion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsDigestRef(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a", true},
|
||||||
|
{"0.45.0", false},
|
||||||
|
{"", false},
|
||||||
|
{"sha256", false},
|
||||||
|
{"SHA256:abc", false}, // case-sensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
if got := isDigestRef(tt.input); got != tt.want {
|
||||||
|
t.Errorf("isDigestRef(%q) = %v, want %v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginDiscoverSkipsBuiltins(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
for _, name := range []string{"flux-get", "flux-create", "flux-version"} {
|
||||||
|
os.WriteFile(pluginDir+"/"+name, []byte("#!/bin/sh"), 0o755)
|
||||||
|
}
|
||||||
|
os.WriteFile(pluginDir+"/flux-myplugin", []byte("#!/bin/sh"), 0o755)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
names := make([]string, len(plugins))
|
||||||
|
for i, p := range plugins {
|
||||||
|
names[i] = p.Name
|
||||||
|
}
|
||||||
|
t.Fatalf("expected 1 plugin, got %d: %v", len(plugins), names)
|
||||||
|
}
|
||||||
|
if plugins[0].Name != "myplugin" {
|
||||||
|
t.Errorf("expected 'myplugin', got %q", plugins[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
cmd/flux/plugin_uninstall.go
Normal file
48
cmd/flux/plugin_uninstall.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginUninstallCmd = &cobra.Command{
|
||||||
|
Use: "uninstall <name>",
|
||||||
|
Aliases: []string{"delete"},
|
||||||
|
Short: "Uninstall a plugin",
|
||||||
|
Long: `The plugin uninstall command removes a plugin binary and its receipt from the plugin directory.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: pluginUninstallCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginUninstallCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginUninstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
pluginDir := pluginHandler.PluginDir()
|
||||||
|
|
||||||
|
if err := plugin.Uninstall(pluginDir, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Successf("uninstalled %s", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
102
cmd/flux/plugin_update.go
Normal file
102
cmd/flux/plugin_update.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if pullArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
if pullArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
opt, err := loginWithProvider(ctx, url, pullArtifactArgs.provider.String())
|
opt, _, err := loginWithProvider(ctx, url, pullArtifactArgs.provider.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ type pushArtifactFlags struct {
|
|||||||
debug bool
|
debug bool
|
||||||
reproducible bool
|
reproducible bool
|
||||||
insecure bool
|
insecure bool
|
||||||
|
resolveSymlinks bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushArtifactArgs = newPushArtifactFlags()
|
var pushArtifactArgs = newPushArtifactFlags()
|
||||||
@@ -137,6 +138,7 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -183,6 +185,15 @@ 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, "=")
|
||||||
@@ -225,11 +236,12 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if provider := pushArtifactArgs.provider.String(); provider != sourcev1.GenericOCIProvider {
|
if provider := pushArtifactArgs.provider.String(); provider != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
authOpt, err := loginWithProvider(ctx, url, provider)
|
var opt crane.Option
|
||||||
|
opt, authenticator, err = loginWithProvider(ctx, url, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
opts = append(opts, authOpt)
|
opts = append(opts, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rootArgs.timeout != 0 {
|
if rootArgs.timeout != 0 {
|
||||||
|
|||||||
@@ -152,7 +152,14 @@ func reconciliationHandled(kubeClient client.Client, namespacedName types.Namesp
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Status == kstatus.CurrentStatus, nil
|
switch result.Status {
|
||||||
|
case kstatus.CurrentStatus:
|
||||||
|
return true, nil
|
||||||
|
case kstatus.InProgressStatus:
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("%s", result.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
|
|
||||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
@@ -67,7 +66,7 @@ func (obj helmReleaseAdapter) reconcileSource() bool {
|
|||||||
return rhrArgs.syncHrWithSource
|
return rhrArgs.syncHrWithSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName) {
|
func (obj helmReleaseAdapter) getSource() (reconcileSource, sourceReference) {
|
||||||
var (
|
var (
|
||||||
name string
|
name string
|
||||||
ns string
|
ns string
|
||||||
@@ -78,21 +77,26 @@ func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName
|
|||||||
if ns == "" {
|
if ns == "" {
|
||||||
ns = obj.Namespace
|
ns = obj.Namespace
|
||||||
}
|
}
|
||||||
namespacedName := types.NamespacedName{
|
srcRef := sourceReference{
|
||||||
Name: name,
|
kind: obj.Spec.ChartRef.Kind,
|
||||||
Namespace: ns,
|
name: name,
|
||||||
|
namespace: ns,
|
||||||
}
|
}
|
||||||
if obj.Spec.ChartRef.Kind == sourcev1.HelmChartKind {
|
switch obj.Spec.ChartRef.Kind {
|
||||||
|
case sourcev1.HelmChartKind:
|
||||||
return reconcileWithSourceCommand{
|
return reconcileWithSourceCommand{
|
||||||
apiType: helmChartType,
|
apiType: helmChartType,
|
||||||
object: helmChartAdapter{&sourcev1.HelmChart{}},
|
object: helmChartAdapter{&sourcev1.HelmChart{}},
|
||||||
force: true,
|
force: true,
|
||||||
}, namespacedName
|
}, srcRef
|
||||||
}
|
case sourcev1.OCIRepositoryKind:
|
||||||
return reconcileCommand{
|
return reconcileCommand{
|
||||||
apiType: ociRepositoryType,
|
apiType: ociRepositoryType,
|
||||||
object: ociRepositoryAdapter{&sourcev1.OCIRepository{}},
|
object: ociRepositoryAdapter{&sourcev1.OCIRepository{}},
|
||||||
}, namespacedName
|
}, srcRef
|
||||||
|
default:
|
||||||
|
return nil, srcRef
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// default case assumes the HelmRelease is using a HelmChartTemplate
|
// default case assumes the HelmRelease is using a HelmChartTemplate
|
||||||
ns = obj.Spec.Chart.Spec.SourceRef.Namespace
|
ns = obj.Spec.Chart.Spec.SourceRef.Namespace
|
||||||
@@ -104,9 +108,10 @@ func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName
|
|||||||
apiType: helmChartType,
|
apiType: helmChartType,
|
||||||
object: helmChartAdapter{&sourcev1.HelmChart{}},
|
object: helmChartAdapter{&sourcev1.HelmChart{}},
|
||||||
force: true,
|
force: true,
|
||||||
}, types.NamespacedName{
|
}, sourceReference{
|
||||||
Name: name,
|
kind: sourcev1.HelmChartKind,
|
||||||
Namespace: ns,
|
name: name,
|
||||||
|
namespace: ns,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
|
|
||||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
@@ -62,8 +61,8 @@ func (obj kustomizationAdapter) reconcileSource() bool {
|
|||||||
return rksArgs.syncKsWithSource
|
return rksArgs.syncKsWithSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj kustomizationAdapter) getSource() (reconcileSource, types.NamespacedName) {
|
func (obj kustomizationAdapter) getSource() (reconcileSource, sourceReference) {
|
||||||
var cmd reconcileCommand
|
var cmd reconcileSource
|
||||||
switch obj.Spec.SourceRef.Kind {
|
switch obj.Spec.SourceRef.Kind {
|
||||||
case sourcev1.OCIRepositoryKind:
|
case sourcev1.OCIRepositoryKind:
|
||||||
cmd = reconcileCommand{
|
cmd = reconcileCommand{
|
||||||
@@ -82,9 +81,10 @@ func (obj kustomizationAdapter) getSource() (reconcileSource, types.NamespacedNa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd, types.NamespacedName{
|
return cmd, sourceReference{
|
||||||
Name: obj.Spec.SourceRef.Name,
|
kind: obj.Spec.SourceRef.Kind,
|
||||||
Namespace: obj.Spec.SourceRef.Namespace,
|
name: obj.Spec.SourceRef.Name,
|
||||||
|
namespace: obj.Spec.SourceRef.Namespace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
)
|
)
|
||||||
@@ -58,8 +57,8 @@ func (obj helmChartAdapter) reconcileSource() bool {
|
|||||||
return rhcArgs.syncHrWithSource
|
return rhcArgs.syncHrWithSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj helmChartAdapter) getSource() (reconcileSource, types.NamespacedName) {
|
func (obj helmChartAdapter) getSource() (reconcileSource, sourceReference) {
|
||||||
var cmd reconcileCommand
|
var cmd reconcileSource
|
||||||
switch obj.Spec.SourceRef.Kind {
|
switch obj.Spec.SourceRef.Kind {
|
||||||
case sourcev1.HelmRepositoryKind:
|
case sourcev1.HelmRepositoryKind:
|
||||||
cmd = reconcileCommand{
|
cmd = reconcileCommand{
|
||||||
@@ -78,9 +77,10 @@ func (obj helmChartAdapter) getSource() (reconcileSource, types.NamespacedName)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd, types.NamespacedName{
|
return cmd, sourceReference{
|
||||||
Name: obj.Spec.SourceRef.Name,
|
kind: obj.Spec.SourceRef.Kind,
|
||||||
Namespace: obj.Namespace,
|
name: obj.Spec.SourceRef.Name,
|
||||||
|
namespace: obj.Namespace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,17 @@ import (
|
|||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type sourceReference struct {
|
||||||
|
kind string
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
}
|
||||||
|
|
||||||
type reconcileWithSource interface {
|
type reconcileWithSource interface {
|
||||||
adapter
|
adapter
|
||||||
reconcilable
|
reconcilable
|
||||||
reconcileSource() bool
|
reconcileSource() bool
|
||||||
getSource() (reconcileSource, types.NamespacedName)
|
getSource() (reconcileSource, sourceReference)
|
||||||
}
|
}
|
||||||
|
|
||||||
type reconcileSource interface {
|
type reconcileSource interface {
|
||||||
@@ -61,14 +67,17 @@ func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reconcile.object.reconcileSource() || reconcile.force {
|
if reconcile.object.reconcileSource() || reconcile.force {
|
||||||
reconcileCmd, nsName := reconcile.object.getSource()
|
reconcileCmd, srcRef := reconcile.object.getSource()
|
||||||
nsCopy := *kubeconfigArgs.Namespace
|
if reconcileCmd == nil {
|
||||||
if nsName.Namespace != "" {
|
return fmt.Errorf("cannot reconcile source of kind %s", srcRef.kind)
|
||||||
*kubeconfigArgs.Namespace = nsName.Namespace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := reconcileCmd.run(nil, []string{nsName.Name})
|
nsCopy := *kubeconfigArgs.Namespace
|
||||||
if err != nil {
|
if srcRef.namespace != "" {
|
||||||
|
*kubeconfigArgs.Namespace = srcRef.namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := reconcileCmd.run(nil, []string{srcRef.name}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*kubeconfigArgs.Namespace = nsCopy
|
*kubeconfigArgs.Namespace = nsCopy
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +262,8 @@ func (resume resumeCommand) printMessage(responses []reconcileResponse) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
logger.Failuref("%s", r.err.Error())
|
logger.Failuref("%s %s reconciliation failed: %s", resume.kind, r.asClientObject().GetName(), r.err.Error())
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
logger.Successf("%s %s reconciliation completed", resume.kind, r.asClientObject().GetName())
|
logger.Successf("%s %s reconciliation completed", resume.kind, r.asClientObject().GetName())
|
||||||
logger.Successf("%s", r.successMessage())
|
logger.Successf("%s", r.successMessage())
|
||||||
|
|||||||
@@ -195,3 +195,37 @@ func (a helmRepositoryListAdapter) asClientList() client.ObjectList {
|
|||||||
func (a helmRepositoryListAdapter) len() int {
|
func (a helmRepositoryListAdapter) len() int {
|
||||||
return len(a.HelmRepositoryList.Items)
|
return len(a.HelmRepositoryList.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourcev1.ExternalArtifact
|
||||||
|
|
||||||
|
var externalArtifactType = apiType{
|
||||||
|
kind: sourcev1.ExternalArtifactKind,
|
||||||
|
humanKind: "source external-artifact",
|
||||||
|
groupVersion: sourcev1.GroupVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
type externalArtifactAdapter struct {
|
||||||
|
*sourcev1.ExternalArtifact
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactAdapter) asClientObject() client.Object {
|
||||||
|
return a.ExternalArtifact
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactAdapter) deepCopyClientObject() client.Object {
|
||||||
|
return a.ExternalArtifact.DeepCopy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourcev1.ExternalArtifactList
|
||||||
|
|
||||||
|
type externalArtifactListAdapter struct {
|
||||||
|
*sourcev1.ExternalArtifactList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactListAdapter) asClientList() client.ObjectList {
|
||||||
|
return a.ExternalArtifactList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a externalArtifactListAdapter) len() int {
|
||||||
|
return len(a.ExternalArtifactList.Items)
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func tagArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if tagArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
if tagArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
opt, err := loginWithProvider(ctx, url, tagArtifactArgs.provider.String())
|
opt, _, err := loginWithProvider(ctx, url, tagArtifactArgs.provider.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
7
cmd/flux/testdata/build-kustomization/configmaps/existing.yaml
vendored
Normal file
7
cmd/flux/testdata/build-kustomization/configmaps/existing.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: existing-config
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
5
cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml
vendored
Normal file
5
cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./existing.yaml
|
||||||
|
- ./new.yaml
|
||||||
7
cmd/flux/testdata/build-kustomization/configmaps/new.yaml
vendored
Normal file
7
cmd/flux/testdata/build-kustomization/configmaps/new.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: new-config
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
7
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml
vendored
Normal file
7
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: app-config
|
||||||
|
namespace: new-ns
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
5
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml
vendored
Normal file
5
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./namespace.yaml
|
||||||
|
- ./configmap.yaml
|
||||||
4
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml
vendored
Normal file
4
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: new-ns
|
||||||
4
cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml
vendored
Normal file
4
cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./namespace.yaml
|
||||||
4
cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml
vendored
Normal file
4
cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: new-ns
|
||||||
2
cmd/flux/testdata/check/check_pre.golden
vendored
2
cmd/flux/testdata/check/check_pre.golden
vendored
@@ -1,3 +1,3 @@
|
|||||||
► checking prerequisites
|
► checking prerequisites
|
||||||
✔ Kubernetes {{ .serverVersion }} >=1.32.0-0
|
✔ Kubernetes {{ .serverVersion }} >=1.33.0-0
|
||||||
✔ prerequisites checks passed
|
✔ prerequisites checks passed
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ metadata:
|
|||||||
namespace: my-namespace
|
namespace: my-namespace
|
||||||
stringData:
|
stringData:
|
||||||
githubAppID: "1"
|
githubAppID: "1"
|
||||||
githubAppInstallationID: "2"
|
githubAppInstallationOwner: my-org
|
||||||
githubAppPrivateKey: |-
|
githubAppPrivateKey: |-
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap
|
YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap
|
||||||
|
|||||||
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml
vendored
Normal file
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
|
||||||
|
name: gcr-secret
|
||||||
|
namespace: my-namespace
|
||||||
|
stringData:
|
||||||
|
audience: https://custom.audience.example.com
|
||||||
|
email: sa@project.iam.gserviceaccount.com
|
||||||
|
token: test-token
|
||||||
|
|
||||||
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml
vendored
Normal file
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
|
||||||
|
name: gcr-secret
|
||||||
|
namespace: my-namespace
|
||||||
|
stringData:
|
||||||
|
audience: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
|
||||||
|
email: sa@project.iam.gserviceaccount.com
|
||||||
|
token: test-token
|
||||||
|
|
||||||
11
cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml
vendored
Normal file
11
cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/106120121d366c2f67e93200f6c1dbe938235eb588daa5e8c0516d3a77ac1dee
|
||||||
|
name: receiver-secret
|
||||||
|
namespace: my-namespace
|
||||||
|
stringData:
|
||||||
|
token: test-token
|
||||||
|
|
||||||
27
cmd/flux/testdata/create_tenant/tenant-with-skip-namespace.yaml
vendored
Normal file
27
cmd/flux/testdata/create_tenant/tenant-with-skip-namespace.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
toolkit.fluxcd.io/tenant: dev-team
|
||||||
|
name: dev-team
|
||||||
|
namespace: apps
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
toolkit.fluxcd.io/tenant: dev-team
|
||||||
|
name: dev-team-reconciler
|
||||||
|
namespace: apps
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: cluster-admin
|
||||||
|
subjects:
|
||||||
|
- apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: User
|
||||||
|
name: gotk:apps:reconciler
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: dev-team
|
||||||
|
namespace: apps
|
||||||
1
cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden
vendored
Normal file
1
cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
► Namespace/new-ns created
|
||||||
9
cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden
vendored
Normal file
9
cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
► 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
|
||||||
7
cmd/flux/testdata/diff-kustomization/existing-configmap.yaml
vendored
Normal file
7
cmd/flux/testdata/diff-kustomization/existing-configmap.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: existing-config
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml
vendored
Normal file
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml
vendored
Normal file
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml
vendored
Normal file
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml
vendored
Normal file
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
12
cmd/flux/testdata/export/external-artifact.yaml
vendored
Normal file
12
cmd/flux/testdata/export/external-artifact.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: ExternalArtifact
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
sourceRef:
|
||||||
|
apiVersion: source.example.com/v1alpha1
|
||||||
|
kind: GitHubRelease
|
||||||
|
name: flux-system
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
12
cmd/flux/testdata/export/objects.yaml
vendored
12
cmd/flux/testdata/export/objects.yaml
vendored
@@ -165,3 +165,15 @@ spec:
|
|||||||
endpoint: s3.amazonaws.com
|
endpoint: s3.amazonaws.com
|
||||||
region: us-east-1
|
region: us-east-1
|
||||||
timeout: 30s
|
timeout: 30s
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: ExternalArtifact
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
sourceRef:
|
||||||
|
apiVersion: source.example.com/v1alpha1
|
||||||
|
kind: GitHubRelease
|
||||||
|
name: flux-system
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
|||||||
0
cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file
vendored
Normal file
0
cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file
vendored
Normal file
1
cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file-link.yaml
vendored
Symbolic link
1
cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file-link.yaml
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
./another-file.yaml
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user