mirror of
https://github.com/fluxcd/flux2.git
synced 2026-07-02 11:55:08 +00:00
Compare commits
164 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
006ecb6b04 | ||
|
|
93174b454f | ||
|
|
c1f7f0b86a | ||
|
|
1275f96e06 | ||
|
|
dcc7def046 | ||
|
|
dcf8165ce5 | ||
|
|
3cacae31e8 | ||
|
|
5df79510e2 | ||
|
|
efc030ffe4 | ||
|
|
31c6ba34f7 | ||
|
|
03af1c5290 | ||
|
|
ca5347f4b4 | ||
|
|
4b7e9eef43 | ||
|
|
bbf064e4fc | ||
|
|
72c2a83e4f | ||
|
|
8daa9b9ddf | ||
|
|
c888f28b2c | ||
|
|
456d935ae1 | ||
|
|
2c1eec5c0f | ||
|
|
542dbb5111 | ||
|
|
4d41251dbc | ||
|
|
9c610bacd2 | ||
|
|
94c079c109 | ||
|
|
c04738c543 | ||
|
|
6fe4f7b502 | ||
|
|
65d4635709 | ||
|
|
cec25b5d1e | ||
|
|
cd0ffe0151 | ||
|
|
f234f2f26f | ||
|
|
5afd1d8728 | ||
|
|
e833099e1d | ||
|
|
b4cf45fc95 | ||
|
|
3e49729349 | ||
|
|
9a68454996 | ||
|
|
9f995dfec0 | ||
|
|
65d975b490 | ||
|
|
96fda4cd56 | ||
|
|
2ca3468423 | ||
|
|
4f45409697 | ||
|
|
923a8ae394 | ||
|
|
4e8c13ba59 | ||
|
|
61316ccca7 | ||
|
|
43574215a6 | ||
|
|
de76bb4725 | ||
|
|
b767c68876 | ||
|
|
a84934311a | ||
|
|
4810828b53 | ||
|
|
e6ac1390d0 | ||
|
|
6f803d47bc | ||
|
|
4e815ab5e2 | ||
|
|
a969646a56 | ||
|
|
1e104631e4 | ||
|
|
44612a750d | ||
|
|
e31c1a4f7d | ||
|
|
8f5b850727 | ||
|
|
7a725fc3ad | ||
|
|
56166fd90c | ||
|
|
c438a10efc | ||
|
|
7a53052d06 | ||
|
|
b1b4438ae9 | ||
|
|
862ab9b370 | ||
|
|
c1355c1e72 | ||
|
|
e0803ee689 | ||
|
|
04b23241e1 | ||
|
|
3aaa5fd4ef | ||
|
|
f265800a87 | ||
|
|
0afcda1a50 | ||
|
|
d78d406a52 | ||
|
|
5999cd4b9a | ||
|
|
3c2fe83dc2 | ||
|
|
9351ff68af | ||
|
|
3fe2820cf0 | ||
|
|
166cc7ca72 | ||
|
|
9daccd1847 | ||
|
|
3e21c27749 | ||
|
|
ed778892df | ||
|
|
22953596c6 | ||
|
|
8c41d5b56d | ||
|
|
4bfdb6d459 | ||
|
|
9d9e56208c | ||
|
|
5425087730 | ||
|
|
fa7cd5f847 | ||
|
|
6d95d5b1a3 | ||
|
|
f75d52d5c6 | ||
|
|
272410d3e9 | ||
|
|
63281daf2f | ||
|
|
4b5a433923 | ||
|
|
abb86f161b | ||
|
|
626bb58a69 | ||
|
|
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 |
154 changed files with 9152 additions and 1206 deletions
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.5.x
|
|
||||||
description: To be backported to release/v2.5.x
|
|
||||||
color: '#ffd700'
|
|
||||||
- name: backport:release/v2.6.x
|
|
||||||
description: To be backported to release/v2.6.x
|
|
||||||
color: '#ffd700'
|
|
||||||
- name: backport:release/v2.7.x
|
- name: backport:release/v2.7.x
|
||||||
description: To be backported to release/v2.7.x
|
description: To be backported to release/v2.7.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
|
- name: backport:release/v2.8.x
|
||||||
|
description: To be backported to release/v2.8.x
|
||||||
|
color: '#ffd700'
|
||||||
|
- name: backport:release/v2.9.x
|
||||||
|
description: To be backported to release/v2.9.x
|
||||||
|
color: '#ffd700'
|
||||||
|
|
|
||||||
40
.github/workflows/action.yaml
vendored
40
.github/workflows/action.yaml
vendored
|
|
@ -24,6 +24,44 @@ jobs:
|
||||||
name: action on ${{ matrix.version }}
|
name: action on ${{ matrix.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup flux
|
- name: Setup flux
|
||||||
uses: ./action
|
uses: ./action
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: action plugins on ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache-dependency-path: go.sum
|
||||||
|
- name: Build flux
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p cmd/flux/manifests "$RUNNER_TEMP/flux-bin"
|
||||||
|
printf "apiVersion: v1\nkind: List\nitems: []\n" > cmd/flux/manifests/dummy.yaml
|
||||||
|
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=0.0.0-pr" -o "$RUNNER_TEMP/flux-bin/flux" ./cmd/flux
|
||||||
|
- name: Setup flux with plugin
|
||||||
|
id: setup
|
||||||
|
uses: ./action
|
||||||
|
with:
|
||||||
|
version: 0.0.0-pr
|
||||||
|
bindir: ${{ runner.temp }}/flux-bin
|
||||||
|
plugins: |
|
||||||
|
mirror@sha256:91a1e04c2015ee66b1633e362cdb6d4550891b91bf3391a83414b9e9912e53f1
|
||||||
|
- name: Assert plugin installation
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
ACTION_PLUGIN_DIR: ${{ steps.setup.outputs['plugin-dir'] }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -n "${FLUXCD_PLUGINS:-}"
|
||||||
|
test "$ACTION_PLUGIN_DIR" = "$FLUXCD_PLUGINS"
|
||||||
|
test -x "$FLUXCD_PLUGINS/flux-mirror"
|
||||||
|
flux plugin list
|
||||||
|
flux mirror --help
|
||||||
|
|
|
||||||
2
.github/workflows/backport.yaml
vendored
2
.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.9.0
|
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
32
.github/workflows/conformance.yaml
vendored
32
.github/workflows/conformance.yaml
vendored
|
|
@ -19,13 +19,13 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
|
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
|
||||||
KUBERNETES_VERSION: [1.33.0, 1.34.1, 1.35.0]
|
KUBERNETES_VERSION: [1.34.1, 1.35.2, 1.36.1]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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: |
|
||||||
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.32.0
|
||||||
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
|
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
|
|
@ -76,13 +76,13 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Available versions can be found with "replicated cluster versions"
|
# Available versions can be found with "replicated cluster versions"
|
||||||
K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ]
|
K3S_VERSION: [ 1.34.8, 1.35.5, 1.36.1 ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
|
|
@ -107,7 +107,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
uses: replicatedhq/replicated-actions/create-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "k3s"
|
kubernetes-distribution: "k3s"
|
||||||
|
|
@ -150,7 +150,7 @@ jobs:
|
||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
uses: replicatedhq/replicated-actions/remove-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
|
|
@ -168,13 +168,13 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
||||||
OPENSHIFT_VERSION: [ 4.20.0-okd ]
|
OPENSHIFT_VERSION: [ 4.20.0-okd, 4.21.0-okd ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.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: |
|
||||||
|
|
@ -189,7 +189,7 @@ jobs:
|
||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
|
|
@ -199,7 +199,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
uses: replicatedhq/replicated-actions/create-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "openshift"
|
kubernetes-distribution: "openshift"
|
||||||
|
|
@ -240,7 +240,7 @@ jobs:
|
||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
uses: replicatedhq/replicated-actions/remove-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
|
|
|
||||||
8
.github/workflows/e2e-azure.yaml
vendored
8
.github/workflows/e2e-azure.yaml
vendored
|
|
@ -29,14 +29,14 @@ jobs:
|
||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: CheckoutD
|
- name: CheckoutD
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: tests/integration/go.sum
|
cache-dependency-path: tests/integration/go.sum
|
||||||
- name: Setup Terraform
|
- name: Setup Terraform
|
||||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||||
- 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
|
||||||
|
|
|
||||||
14
.github/workflows/e2e-bootstrap.yaml
vendored
14
.github/workflows/e2e-bootstrap.yaml
vendored
|
|
@ -17,9 +17,9 @@ jobs:
|
||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
|
|
@ -28,16 +28,16 @@ jobs:
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.32.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
# The versions below should target the newest Kubernetes version
|
# The versions below should target the newest Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.36.1-amd64
|
||||||
kubectl_version: v1.33.0
|
kubectl_version: v1.36.0
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Setup yq
|
- name: Setup yq
|
||||||
uses: fluxcd/pkg/actions/yq@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/yq@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
|
|
|
||||||
12
.github/workflows/e2e-gcp.yaml
vendored
12
.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: tests/integration/go.sum
|
cache-dependency-path: tests/integration/go.sum
|
||||||
- name: Setup Terraform
|
- name: Setup Terraform
|
||||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./
|
working-directory: ./
|
||||||
|
|
@ -56,11 +56,11 @@ jobs:
|
||||||
- name: Setup gcloud
|
- name: Setup gcloud
|
||||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- name: Log into us-central1-docker.pkg.dev
|
- name: Log into us-central1-docker.pkg.dev
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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
|
|
@ -18,14 +18,14 @@ jobs:
|
||||||
labels: ubuntu-latest-16-cores
|
labels: ubuntu-latest-16-cores
|
||||||
services:
|
services:
|
||||||
registry:
|
registry:
|
||||||
image: registry:2
|
image: registry:3
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
|
|
@ -34,19 +34,19 @@ jobs:
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.32.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
wait: 5s
|
wait: 5s
|
||||||
config: .github/kind/config.yaml # disable KIND-net
|
config: .github/kind/config.yaml # disable KIND-net
|
||||||
# The versions below should target the oldest supported Kubernetes version
|
# The versions below should target the oldest supported Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.34.1-amd64
|
||||||
kubectl_version: v1.33.0
|
kubectl_version: v1.34.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.32.0/manifests/calico.yaml
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
|
|
|
||||||
6
.github/workflows/ossf.yaml
vendored
6
.github/workflows/ossf.yaml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Run analysis
|
- name: Run analysis
|
||||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||||
with:
|
with:
|
||||||
|
|
@ -28,12 +28,12 @@ jobs:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_results: true
|
publish_results: true
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@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@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
|
||||||
32
.github/workflows/release.yaml
vendored
32
.github/workflows/release.yaml
vendored
|
|
@ -22,35 +22,35 @@ jobs:
|
||||||
packages: write # needed for ghcr access
|
packages: write # needed for ghcr access
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Unshallow
|
- name: Unshallow
|
||||||
run: git fetch --prune --unshallow
|
run: git fetch --prune --unshallow
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
cache: false
|
cache: false
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- name: Setup Syft
|
- name: Setup Syft
|
||||||
uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
|
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
- name: Setup Cosign
|
- name: Setup Cosign
|
||||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||||
with:
|
with:
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
|
|
@ -63,7 +63,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
kustomize build manifests/crds > all-crds.yaml
|
kustomize build manifests/crds > all-crds.yaml
|
||||||
- name: Generate OpenAPI JSON schemas from CRDs
|
- name: Generate OpenAPI JSON schemas from CRDs
|
||||||
uses: fluxcd/pkg/actions/crdjsonschema@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/crdjsonschema@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
with:
|
with:
|
||||||
crd: all-crds.yaml
|
crd: all-crds.yaml
|
||||||
output: schemas
|
output: schemas
|
||||||
|
|
@ -72,7 +72,7 @@ jobs:
|
||||||
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: run-goreleaser
|
id: run-goreleaser
|
||||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --skip=validate
|
args: release --skip=validate
|
||||||
|
|
@ -103,9 +103,9 @@ jobs:
|
||||||
id-token: write
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
uses: ./action/
|
uses: ./action/
|
||||||
with:
|
with:
|
||||||
|
|
@ -116,13 +116,13 @@ jobs:
|
||||||
VERSION=$(flux version --client | awk '{ print $NF }')
|
VERSION=$(flux version --client | awk '{ print $NF }')
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
|
|
@ -150,7 +150,7 @@ jobs:
|
||||||
--path="./flux-system" \
|
--path="./flux-system" \
|
||||||
--source=${{ github.repositoryUrl }} \
|
--source=${{ github.repositoryUrl }} \
|
||||||
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
||||||
- uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||||
with:
|
with:
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Sign manifests
|
- name: Sign manifests
|
||||||
|
|
|
||||||
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.9.0
|
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.11.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.9.0
|
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
6
.github/workflows/update.yaml
vendored
6
.github/workflows/update.yaml
vendored
|
|
@ -16,9 +16,9 @@ jobs:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
|
|
@ -96,7 +96,7 @@ jobs:
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
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: |
|
||||||
|
|
|
||||||
5
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
5
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
|
|
@ -3,8 +3,11 @@ name: upgrade-fluxcd-pkg
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upgrade-fluxcd-pkg:
|
upgrade-fluxcd-pkg:
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
|
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
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-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets)
|
|
||||||
- [source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns
|
|
||||||
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
|
||||||
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
|
|
||||||
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
|
|
||||||
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
|
|
||||||
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
|
|
||||||
|
|
||||||
### Understanding the code
|
|
||||||
|
|
||||||
To get started with developing controllers, you might want to review
|
|
||||||
[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
|
|
||||||
walks you through writing a short and concise controller that watches out
|
|
||||||
for source changes.
|
|
||||||
|
|
||||||
## How to run the test suite
|
|
||||||
|
|
||||||
Prerequisites:
|
|
||||||
|
|
||||||
* go >= 1.26
|
|
||||||
* kubectl >= 1.33
|
|
||||||
* kustomize >= 5.0
|
|
||||||
|
|
||||||
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install-envtest
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run the unit tests with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
|
|
||||||
create a cluster for testing with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make setup-kind
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run the end-to-end tests with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
When the output of the Flux CLI changes, to automatically update the golden
|
|
||||||
files used in the test, pass `-update` flag to the test as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make e2e TEST_ARGS="-update"
|
|
||||||
```
|
|
||||||
|
|
||||||
Since not all packages use golden files for testing, `-update` argument must be
|
|
||||||
passed only for the packages that use golden files. Use the variables
|
|
||||||
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
|
|
||||||
path of the target test package:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unit test
|
|
||||||
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
|
||||||
# e2e test
|
|
||||||
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
|
||||||
```
|
|
||||||
|
|
||||||
Teardown the e2e environment with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make cleanup-kind
|
|
||||||
```
|
|
||||||
|
|
||||||
## Acceptance policy
|
## Acceptance policy
|
||||||
|
|
||||||
These things will make a PR more likely to be accepted:
|
These things will make a PR more likely to be accepted:
|
||||||
|
|
||||||
- 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).
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ FROM alpine:3.23 AS builder
|
||||||
RUN apk add --no-cache ca-certificates curl
|
RUN apk add --no-cache ca-certificates curl
|
||||||
|
|
||||||
ARG ARCH=linux/amd64
|
ARG ARCH=linux/amd64
|
||||||
ARG KUBECTL_VER=1.35.0
|
ARG KUBECTL_VER=1.36.1
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
5
Makefile
5
Makefile
|
|
@ -2,8 +2,9 @@ VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | head -n
|
||||||
DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s)
|
DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s)
|
||||||
EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done
|
EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done
|
||||||
TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig
|
TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig
|
||||||
# Architecture to use envtest with
|
# Architecture to use envtest with; defaults to the host architecture.
|
||||||
ENVTEST_ARCH ?= amd64
|
LOCALARCH ?= $(shell go env GOARCH)
|
||||||
|
ENVTEST_ARCH ?= $(LOCALARCH)
|
||||||
|
|
||||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||||
ifeq (,$(shell go env GOBIN))
|
ifeq (,$(shell go env GOBIN))
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,26 @@ steps:
|
||||||
run: flux version --client
|
run: flux version --client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To install Flux plugins alongside the CLI:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: Setup Flux CLI
|
||||||
|
uses: fluxcd/flux2/action@main
|
||||||
|
with:
|
||||||
|
plugins: |
|
||||||
|
mirror@0.0.1
|
||||||
|
- name: Run Flux plugin
|
||||||
|
run: flux mirror --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Installing plugins requires a Flux version with plugin support (v2.9.0 or later).
|
||||||
|
The `plugins` input accepts one plugin per line in `<name>`,
|
||||||
|
`<name>@<version>`, or `<name>@<digest>` format. Entries without a version or
|
||||||
|
digest install the latest version from the catalog. The `plugin-dir` input is
|
||||||
|
only used when `plugins` is set; when plugins are installed, the action exports
|
||||||
|
`FLUXCD_PLUGINS` for subsequent steps.
|
||||||
|
|
||||||
The Flux GitHub Action can be used to automate various tasks in CI, such as:
|
The Flux GitHub Action can be used to automate various tasks in CI, such as:
|
||||||
|
|
||||||
- [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates)
|
- [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates)
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,36 @@ inputs:
|
||||||
description: "arch can be amd64, arm64 or arm"
|
description: "arch can be amd64, arm64 or arm"
|
||||||
required: false
|
required: false
|
||||||
deprecationMessage: "No longer required, action will now detect runner arch."
|
deprecationMessage: "No longer required, action will now detect runner arch."
|
||||||
|
plugins:
|
||||||
|
description: >
|
||||||
|
Plugins to install alongside Flux. One per line: `<name>`, `<name>@<version>` or
|
||||||
|
`<name>@<digest>`. Entries without a version or digest install the latest
|
||||||
|
version from the catalog. Example: `mirror@0.0.1`
|
||||||
|
required: false
|
||||||
|
plugin-dir:
|
||||||
|
description: >
|
||||||
|
Directory where requested plugins are installed. This input is only used when
|
||||||
|
`plugins` is set. When plugins are installed, the action exports
|
||||||
|
`FLUXCD_PLUGINS` for subsequent steps. Defaults to a `plugins` directory
|
||||||
|
alongside the Flux binary in $RUNNER_TOOL_CACHE.
|
||||||
|
required: false
|
||||||
bindir:
|
bindir:
|
||||||
description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
|
description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
|
||||||
required: false
|
required: false
|
||||||
token:
|
token:
|
||||||
description: "Token used to authenticate against the GitHub.com API."
|
description: "Token used to authenticate against the GitHub.com API."
|
||||||
required: false
|
required: false
|
||||||
|
outputs:
|
||||||
|
plugin-dir:
|
||||||
|
description: >
|
||||||
|
Directory where the plugins were installed by the action.
|
||||||
|
value: ${{ steps.set-outputs.outputs.plugin-dir }}
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: "Download the binary to the runner's cache dir"
|
- name: "Download the binary to the runner's cache dir"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
id: setup-flux-bin
|
||||||
env:
|
env:
|
||||||
VERSION: "${{ inputs.version }}"
|
VERSION: "${{ inputs.version }}"
|
||||||
FLUX_TOOL_DIR: "${{ inputs.bindir }}"
|
FLUX_TOOL_DIR: "${{ inputs.bindir }}"
|
||||||
|
|
@ -42,11 +61,13 @@ runs:
|
||||||
if [[ $VERSION = v* ]]; then
|
if [[ $VERSION = v* ]]; then
|
||||||
VERSION="${VERSION:1}"
|
VERSION="${VERSION:1}"
|
||||||
fi
|
fi
|
||||||
|
echo "installed-flux-version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')
|
OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')
|
||||||
if [[ "$OS" == "macos" ]]; then
|
if [[ "$OS" == "macos" ]]; then
|
||||||
OS="darwin"
|
OS="darwin"
|
||||||
fi
|
fi
|
||||||
|
echo "os=$OS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')
|
ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')
|
||||||
if [[ "$ARCH" == "x64" ]]; then
|
if [[ "$ARCH" == "x64" ]]; then
|
||||||
|
|
@ -54,6 +75,7 @@ runs:
|
||||||
elif [[ "$ARCH" == "x86" ]]; then
|
elif [[ "$ARCH" == "x86" ]]; then
|
||||||
ARCH="386"
|
ARCH="386"
|
||||||
fi
|
fi
|
||||||
|
echo "arch=$ARCH" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
FLUX_EXEC_FILE="flux"
|
FLUX_EXEC_FILE="flux"
|
||||||
if [[ "$OS" == "windows" ]]; then
|
if [[ "$OS" == "windows" ]]; then
|
||||||
|
|
@ -63,7 +85,7 @@ runs:
|
||||||
if [[ -z "$FLUX_TOOL_DIR" ]]; then
|
if [[ -z "$FLUX_TOOL_DIR" ]]; then
|
||||||
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
|
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
|
||||||
fi
|
fi
|
||||||
if [[ ! -x "$FLUX_TOOL_DIR/FLUX_EXEC_FILE" ]]; then
|
if [[ ! -x "$FLUX_TOOL_DIR/$FLUX_EXEC_FILE" ]]; then
|
||||||
DL_DIR="$(mktemp -dt flux2-XXXXXX)"
|
DL_DIR="$(mktemp -dt flux2-XXXXXX)"
|
||||||
trap 'rm -rf $DL_DIR' EXIT
|
trap 'rm -rf $DL_DIR' EXIT
|
||||||
|
|
||||||
|
|
@ -146,3 +168,63 @@ runs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
flux -v
|
flux -v
|
||||||
|
|
||||||
|
- name: "Plugin support gate"
|
||||||
|
id: plugin-support-gate
|
||||||
|
shell: bash
|
||||||
|
if: inputs.plugins != ''
|
||||||
|
run: |
|
||||||
|
flux_version="$(flux -v 2>/dev/null | awk '{print $3}')"
|
||||||
|
if flux plugin --help >/dev/null 2>&1; then
|
||||||
|
echo "plugin-support=yes" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "plugin-support=no" >> "$GITHUB_OUTPUT"
|
||||||
|
msg="Installed Flux version ${flux_version:-unknown} does not support plugins; need >= 2.9.0. Requested plugins cannot be installed."
|
||||||
|
echo "::error title=Unsupported Flux version::$msg"
|
||||||
|
echo "> [!CAUTION]" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "> $msg" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Setup Plugins Dir"
|
||||||
|
shell: bash
|
||||||
|
if: steps.plugin-support-gate.outputs.plugin-support == 'yes'
|
||||||
|
env:
|
||||||
|
FLUXCD_PLUGINS: ${{ inputs.plugin-dir }}
|
||||||
|
VERSION: ${{ steps.setup-flux-bin.outputs.installed-flux-version }}
|
||||||
|
OS: ${{ steps.setup-flux-bin.outputs.os }}
|
||||||
|
ARCH: ${{ steps.setup-flux-bin.outputs.arch }}
|
||||||
|
run: |
|
||||||
|
# Use a default plugin directory when plugin-dir was not provided.
|
||||||
|
if [[ -z "$FLUXCD_PLUGINS" ]]; then
|
||||||
|
FLUXCD_PLUGINS="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}/plugins/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export it so subsequent steps can discover the installed plugins.
|
||||||
|
echo "FLUXCD_PLUGINS=$FLUXCD_PLUGINS" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# Create the directory if it does not exist.
|
||||||
|
mkdir -p "$FLUXCD_PLUGINS"
|
||||||
|
|
||||||
|
- name: "Install Plugins"
|
||||||
|
shell: bash
|
||||||
|
id: install-plugins
|
||||||
|
if: steps.plugin-support-gate.outputs.plugin-support == 'yes'
|
||||||
|
env:
|
||||||
|
PLUGINS: ${{ inputs.plugins }}
|
||||||
|
run: |
|
||||||
|
echo "$PLUGINS" | while read -r PLUGIN; do
|
||||||
|
trimmed="${PLUGIN//[[:space:]]/}"
|
||||||
|
if [[ -n "$trimmed" ]]; then
|
||||||
|
echo Installing: "$PLUGIN"
|
||||||
|
flux plugin install "$PLUGIN"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: "Set Output"
|
||||||
|
id: set-outputs
|
||||||
|
if: steps.plugin-support-gate.outputs.plugin-support == 'yes'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "plugin-dir=$FLUXCD_PLUGINS" >> "$GITHUB_OUTPUT"
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
@ -30,6 +32,7 @@ import (
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/bootstrap"
|
||||||
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
|
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
|
||||||
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
||||||
)
|
)
|
||||||
|
|
@ -47,6 +50,7 @@ type bootstrapFlags struct {
|
||||||
|
|
||||||
branch string
|
branch string
|
||||||
recurseSubmodules bool
|
recurseSubmodules bool
|
||||||
|
sparseCheckout []string
|
||||||
manifestsPath string
|
manifestsPath string
|
||||||
|
|
||||||
defaultComponents []string
|
defaultComponents []string
|
||||||
|
|
@ -79,6 +83,11 @@ type bootstrapFlags struct {
|
||||||
gpgPassphrase string
|
gpgPassphrase string
|
||||||
gpgKeyID string
|
gpgKeyID string
|
||||||
|
|
||||||
|
sshSigningKeyFile string
|
||||||
|
sshSigningPassword string
|
||||||
|
sshSigningPassphrase string
|
||||||
|
sshSigningReusePrivateKey bool
|
||||||
|
|
||||||
force bool
|
force bool
|
||||||
|
|
||||||
commitMessageAppendix string
|
commitMessageAppendix string
|
||||||
|
|
@ -109,6 +118,8 @@ func init() {
|
||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch")
|
||||||
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false,
|
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false,
|
||||||
"when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces")
|
"when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces")
|
||||||
|
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.sparseCheckout, "sparse-checkout", nil,
|
||||||
|
"list of directories to be included in the GitRepository sparse checkout, the configured --path must be one of them, accepts comma-separated values")
|
||||||
|
|
||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
|
||||||
|
|
||||||
|
|
@ -139,6 +150,12 @@ func init() {
|
||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key")
|
||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
|
||||||
|
|
||||||
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningKeyFile, "ssh-signing-key-file", "", "path to an SSH private key file used for signing commits")
|
||||||
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningPassword, "ssh-signing-password", "", "passphrase for decrypting SSH signing key")
|
||||||
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningPassphrase, "ssh-signing-passphrase", "", "alias for --ssh-signing-password")
|
||||||
|
bootstrapCmd.PersistentFlags().MarkHidden("ssh-signing-passphrase")
|
||||||
|
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.sshSigningReusePrivateKey, "ssh-signing-reuse-private-key", false, "use the SSH transport key (--private-key-file) to sign commits")
|
||||||
|
|
||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
|
||||||
|
|
||||||
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
|
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
|
||||||
|
|
@ -195,6 +212,31 @@ func bootstrapValidate() error {
|
||||||
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
|
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sshSigningSet := bootstrapArgs.sshSigningKeyFile != "" || bootstrapArgs.sshSigningReusePrivateKey
|
||||||
|
if bootstrapArgs.gpgKeyRingPath != "" && sshSigningSet {
|
||||||
|
return fmt.Errorf("--gpg-* and --ssh-signing-* are mutually exclusive; pick one signing format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" && bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
return fmt.Errorf("--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey && bootstrapArgs.privateKeyFile == "" {
|
||||||
|
return fmt.Errorf("--ssh-signing-reuse-private-key requires --private-key-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
sshSigningPwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sshSigningPwd != "" && bootstrapArgs.sshSigningKeyFile == "" {
|
||||||
|
return fmt.Errorf("--ssh-signing-password requires --ssh-signing-key-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := preflightSigningKey(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 {
|
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 {
|
||||||
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
|
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
|
||||||
}
|
}
|
||||||
|
|
@ -214,6 +256,57 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preflightSigningKey reads and parses the configured signing key so
|
||||||
|
// malformed PEM, wrong passphrases, and unsupported SSH algorithms
|
||||||
|
// surface before any clone runs.
|
||||||
|
func preflightSigningKey() error {
|
||||||
|
switch {
|
||||||
|
case bootstrapArgs.gpgKeyRingPath != "":
|
||||||
|
ring, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid GPG signing key: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := bootstrap.SelectOpenPGPSigningEntity(ring, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID); err != nil {
|
||||||
|
return fmt.Errorf("invalid GPG signing key: %w", err)
|
||||||
|
}
|
||||||
|
case bootstrapArgs.sshSigningKeyFile != "":
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(pwd)); err != nil {
|
||||||
|
return fmt.Errorf("invalid SSH signing key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// effectiveSshSigningPassword resolves the SSH signing-key passphrase
|
||||||
|
// from --ssh-signing-password and its hidden alias
|
||||||
|
// --ssh-signing-passphrase. When both are set with the same value, the
|
||||||
|
// value is returned. When both are set with different non-empty values,
|
||||||
|
// an error is returned. When neither is set, an empty string is
|
||||||
|
// returned with no error.
|
||||||
|
func effectiveSshSigningPassword() (string, error) {
|
||||||
|
pw := bootstrapArgs.sshSigningPassword
|
||||||
|
alias := bootstrapArgs.sshSigningPassphrase
|
||||||
|
switch {
|
||||||
|
case pw != "" && alias != "":
|
||||||
|
if pw != alias {
|
||||||
|
return "", fmt.Errorf("--ssh-signing-password and --ssh-signing-passphrase are aliases; do not pass both")
|
||||||
|
}
|
||||||
|
return pw, nil
|
||||||
|
case pw == "" && alias != "":
|
||||||
|
return alias, nil
|
||||||
|
default:
|
||||||
|
return pw, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation.
|
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation.
|
||||||
// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation
|
// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation
|
||||||
func confirmBootstrap(ctx context.Context, kubeClient client.Client) error {
|
func confirmBootstrap(ctx context.Context, kubeClient client.Client) error {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/gogit"
|
"github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
|
|
@ -253,6 +254,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
TargetPath: bServerArgs.path.ToSlash(),
|
TargetPath: bServerArgs.path.ToSlash(),
|
||||||
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
||||||
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
||||||
|
SparseCheckout: bootstrapArgs.sparseCheckout,
|
||||||
}
|
}
|
||||||
|
|
||||||
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
|
@ -287,6 +289,31 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read transport private key for signing: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
|
||||||
|
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,12 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/auth"
|
||||||
|
"github.com/fluxcd/pkg/auth/aws"
|
||||||
|
authutils "github.com/fluxcd/pkg/auth/utils"
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/gogit"
|
"github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
|
@ -62,9 +66,12 @@ command will perform an upgrade if needed.`,
|
||||||
# Run bootstrap for a Git repository with a private key and password
|
# Run bootstrap for a Git repository with a private key and password
|
||||||
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --password=<password> --path=clusters/my-cluster
|
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --password=<password> --path=clusters/my-cluster
|
||||||
|
|
||||||
# Run bootstrap for a Git repository on AWS CodeCommit
|
# Run bootstrap for a Git repository on AWS CodeCommit using SSH
|
||||||
flux bootstrap git --url=ssh://<SSH-Key-ID>@git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --private-key-file=<path/to/private.key> --password=<SSH-passphrase> --path=clusters/my-cluster
|
flux bootstrap git --url=ssh://<SSH-Key-ID>@git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --private-key-file=<path/to/private.key> --password=<SSH-passphrase> --path=clusters/my-cluster
|
||||||
|
|
||||||
|
# Run bootstrap for a Git repository on AWS CodeCommit using HTTPS (requires AWS IAM credentials)
|
||||||
|
flux bootstrap git --url=https://git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --path=clusters/my-cluster
|
||||||
|
|
||||||
# Run bootstrap for a Git repository on Azure Devops
|
# Run bootstrap for a Git repository on Azure Devops
|
||||||
flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --private-key-file=<path/to/rsa-sha2-private.key> --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster
|
flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --private-key-file=<path/to/rsa-sha2-private.key> --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster
|
||||||
|
|
||||||
|
|
@ -109,6 +116,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrapArgs.tokenAuth = true
|
bootstrapArgs.tokenAuth = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gitProvider string
|
||||||
gitPassword := os.Getenv(gitPasswordEnvVar)
|
gitPassword := os.Getenv(gitPasswordEnvVar)
|
||||||
if gitPassword != "" && gitArgs.password == "" {
|
if gitPassword != "" && gitArgs.password == "" {
|
||||||
gitArgs.password = gitPassword
|
gitArgs.password = gitPassword
|
||||||
|
|
@ -131,8 +139,12 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") {
|
if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") {
|
||||||
if repositoryURL.Scheme == string(git.SSH) {
|
// https://docs.aws.amazon.com/codecommit/latest/userguide/auth-and-access-control.html
|
||||||
|
if repositoryURL.Scheme == string(git.SSH) { // IAM user + SSH
|
||||||
if repositoryURL.User == nil {
|
if repositoryURL.User == nil {
|
||||||
return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be specified in the url")
|
return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be specified in the url")
|
||||||
}
|
}
|
||||||
|
|
@ -142,14 +154,18 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
if bootstrapArgs.privateKeyFile == "" {
|
if bootstrapArgs.privateKeyFile == "" {
|
||||||
return fmt.Errorf("private key file is required for bootstrapping against AWS CodeCommit using ssh")
|
return fmt.Errorf("private key file is required for bootstrapping against AWS CodeCommit using ssh")
|
||||||
}
|
}
|
||||||
|
} else if repositoryURL.Scheme == string(git.HTTPS) && !bootstrapArgs.tokenAuth { // IAM role + HTTPS
|
||||||
|
creds, err := authutils.GetGitCredentials(ctx, "aws", auth.WithGitURL(*repositoryURL))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get AWS CodeCommit IAM git credentials: %w", err)
|
||||||
|
}
|
||||||
|
gitArgs.username = creds.Username
|
||||||
|
gitArgs.password = creds.Password
|
||||||
|
bootstrapArgs.tokenAuth = true
|
||||||
|
gitProvider = aws.ProviderName
|
||||||
}
|
}
|
||||||
if repositoryURL.Scheme == string(git.HTTPS) && !bootstrapArgs.tokenAuth {
|
|
||||||
return fmt.Errorf("--token-auth=true must be specified for using an HTTPS AWS CodeCommit url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
}
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
|
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -296,6 +312,10 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
TargetPath: gitArgs.path.ToSlash(),
|
TargetPath: gitArgs.path.ToSlash(),
|
||||||
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
||||||
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
||||||
|
SparseCheckout: bootstrapArgs.sparseCheckout,
|
||||||
|
}
|
||||||
|
if gitProvider != "" {
|
||||||
|
syncOpts.Provider = gitProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
|
@ -315,6 +335,33 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read transport private key for signing: %w", err)
|
||||||
|
}
|
||||||
|
// Reuse-path pre-flight: bootstrapValidate cannot run this check
|
||||||
|
// because the SSH transport password is subcommand-local.
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
|
||||||
|
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,11 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
return fmt.Errorf("--ssh-signing-reuse-private-key is not supported by 'bootstrap gitea'; " +
|
||||||
|
"that subcommand generates the SSH transport key in-process and has no operator-supplied key to reuse")
|
||||||
|
}
|
||||||
|
|
||||||
gtToken := os.Getenv(gtTokenEnvVar)
|
gtToken := os.Getenv(gtTokenEnvVar)
|
||||||
if gtToken == "" {
|
if gtToken == "" {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -232,6 +237,7 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
TargetPath: giteaArgs.path.ToSlash(),
|
TargetPath: giteaArgs.path.ToSlash(),
|
||||||
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
||||||
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
||||||
|
SparseCheckout: bootstrapArgs.sparseCheckout,
|
||||||
}
|
}
|
||||||
|
|
||||||
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
|
@ -252,6 +258,7 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrap.WithLogger(logger),
|
bootstrap.WithLogger(logger),
|
||||||
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if bootstrapArgs.sshHostname != "" {
|
if bootstrapArgs.sshHostname != "" {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
||||||
}
|
}
|
||||||
|
|
@ -265,6 +272,19 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,11 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
return fmt.Errorf("--ssh-signing-reuse-private-key is not supported by 'bootstrap github'; " +
|
||||||
|
"that subcommand generates the SSH transport key in-process and has no operator-supplied key to reuse")
|
||||||
|
}
|
||||||
|
|
||||||
ghToken := os.Getenv(ghTokenEnvVar)
|
ghToken := os.Getenv(ghTokenEnvVar)
|
||||||
if ghToken == "" {
|
if ghToken == "" {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -239,6 +244,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
TargetPath: githubArgs.path.ToSlash(),
|
TargetPath: githubArgs.path.ToSlash(),
|
||||||
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
||||||
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
||||||
|
SparseCheckout: bootstrapArgs.sparseCheckout,
|
||||||
}
|
}
|
||||||
|
|
||||||
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
|
@ -259,6 +265,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrap.WithLogger(logger),
|
bootstrap.WithLogger(logger),
|
||||||
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if bootstrapArgs.sshHostname != "" {
|
if bootstrapArgs.sshHostname != "" {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
||||||
}
|
}
|
||||||
|
|
@ -272,6 +279,19 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/fluxcd/go-git-providers/gitprovider"
|
"github.com/fluxcd/go-git-providers/gitprovider"
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/gogit"
|
"github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
|
|
@ -287,6 +288,7 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
TargetPath: gitlabArgs.path.ToSlash(),
|
TargetPath: gitlabArgs.path.ToSlash(),
|
||||||
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
||||||
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
|
||||||
|
SparseCheckout: bootstrapArgs.sparseCheckout,
|
||||||
}
|
}
|
||||||
|
|
||||||
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
|
@ -321,6 +323,31 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read transport private key for signing: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
|
||||||
|
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
208
cmd/flux/bootstrap_test.go
Normal file
208
cmd/flux/bootstrap_test.go
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBootstrapValidate_signingFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
gpgRing string
|
||||||
|
gpgPass string
|
||||||
|
sshKey string
|
||||||
|
sshPass string
|
||||||
|
sshPassp string
|
||||||
|
privateKey string
|
||||||
|
reuse bool
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "no signing flags is valid"},
|
||||||
|
{name: "GPG only is valid", gpgRing: "./testdata/bootstrap/gpg.pgp"},
|
||||||
|
{name: "SSH only is valid", sshKey: "./testdata/bootstrap/ed25519.private"},
|
||||||
|
{
|
||||||
|
name: "Reuse-private-key with private-key-file is valid",
|
||||||
|
privateKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
reuse: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GPG + SSH errors",
|
||||||
|
gpgRing: "./testdata/bootstrap/gpg.pgp",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GPG + reuse errors",
|
||||||
|
gpgRing: "./testdata/bootstrap/gpg.pgp",
|
||||||
|
privateKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
reuse: true,
|
||||||
|
wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH key-file + reuse errors",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
privateKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
reuse: true,
|
||||||
|
wantErr: "--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reuse without private-key-file errors",
|
||||||
|
reuse: true,
|
||||||
|
wantErr: "--ssh-signing-reuse-private-key requires --private-key-file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH password without key errors",
|
||||||
|
sshPass: "secret",
|
||||||
|
wantErr: "--ssh-signing-password requires --ssh-signing-key-file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH passphrase alias alone applies",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
sshPassp: "abcde12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH password and passphrase with same value passes",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
sshPass: "abcde12345",
|
||||||
|
sshPassp: "abcde12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH password and passphrase with different values errors",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
sshPass: "right",
|
||||||
|
sshPassp: "wrong",
|
||||||
|
wantErr: "are aliases; do not pass both",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH malformed key fails pre-flight",
|
||||||
|
sshKey: "./testdata/bootstrap/malformed.private",
|
||||||
|
wantErr: "invalid SSH signing key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH encrypted key without password fails pre-flight",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
wantErr: "passphrase required",
|
||||||
|
},
|
||||||
|
// The GPG fixture used here is encrypted (passphrase: "right") so that
|
||||||
|
// passing the wrong passphrase exercises the Decrypt error path.
|
||||||
|
// An unencrypted key would make Decrypt a no-op regardless of the
|
||||||
|
// passphrase supplied.
|
||||||
|
{
|
||||||
|
name: "GPG with wrong passphrase fails pre-flight",
|
||||||
|
gpgRing: "./testdata/bootstrap/gpg-encrypted.pgp",
|
||||||
|
gpgPass: "wrong",
|
||||||
|
wantErr: "invalid GPG signing key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
savedDefaultComponents := bootstrapArgs.defaultComponents
|
||||||
|
savedGpgRing := bootstrapArgs.gpgKeyRingPath
|
||||||
|
savedGpgPass := bootstrapArgs.gpgPassphrase
|
||||||
|
savedSshKey := bootstrapArgs.sshSigningKeyFile
|
||||||
|
savedSshPass := bootstrapArgs.sshSigningPassword
|
||||||
|
savedSshPassp := bootstrapArgs.sshSigningPassphrase
|
||||||
|
savedPrivKey := bootstrapArgs.privateKeyFile
|
||||||
|
savedReuse := bootstrapArgs.sshSigningReusePrivateKey
|
||||||
|
defer func() {
|
||||||
|
bootstrapArgs.defaultComponents = savedDefaultComponents
|
||||||
|
bootstrapArgs.gpgKeyRingPath = savedGpgRing
|
||||||
|
bootstrapArgs.gpgPassphrase = savedGpgPass
|
||||||
|
bootstrapArgs.sshSigningKeyFile = savedSshKey
|
||||||
|
bootstrapArgs.sshSigningPassword = savedSshPass
|
||||||
|
bootstrapArgs.sshSigningPassphrase = savedSshPassp
|
||||||
|
bootstrapArgs.privateKeyFile = savedPrivKey
|
||||||
|
bootstrapArgs.sshSigningReusePrivateKey = savedReuse
|
||||||
|
}()
|
||||||
|
|
||||||
|
// The e2e TestMain calls resetCmdArgs which clears the
|
||||||
|
// cobra-populated default components, so seed them here to
|
||||||
|
// satisfy the requiredComponents pre-check in bootstrapValidate.
|
||||||
|
bootstrapArgs.defaultComponents = bootstrapArgs.requiredComponents
|
||||||
|
bootstrapArgs.gpgKeyRingPath = tt.gpgRing
|
||||||
|
bootstrapArgs.gpgPassphrase = tt.gpgPass
|
||||||
|
bootstrapArgs.sshSigningKeyFile = tt.sshKey
|
||||||
|
bootstrapArgs.sshSigningPassword = tt.sshPass
|
||||||
|
bootstrapArgs.sshSigningPassphrase = tt.sshPassp
|
||||||
|
bootstrapArgs.privateKeyFile = tt.privateKey
|
||||||
|
bootstrapArgs.sshSigningReusePrivateKey = tt.reuse
|
||||||
|
|
||||||
|
err := bootstrapValidate()
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Providers that generate the SSH transport key in-process (github, gitea)
|
||||||
|
// must reject --ssh-signing-reuse-private-key with their own, provider-
|
||||||
|
// specific error before bootstrapValidate runs — otherwise the generic
|
||||||
|
// "--ssh-signing-reuse-private-key requires --private-key-file" error
|
||||||
|
// shadows the fact that the flag is fundamentally unsupported there.
|
||||||
|
func TestBootstrapProviderRejectsReuseBeforeValidate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
runE func() error
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "github rejects reuse with provider-specific error",
|
||||||
|
runE: func() error { return bootstrapGitHubCmdRun(nil, nil) },
|
||||||
|
wantErr: "not supported by 'bootstrap github'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gitea rejects reuse with provider-specific error",
|
||||||
|
runE: func() error { return bootstrapGiteaCmdRun(nil, nil) },
|
||||||
|
wantErr: "not supported by 'bootstrap gitea'",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
savedReuse := bootstrapArgs.sshSigningReusePrivateKey
|
||||||
|
savedPrivKey := bootstrapArgs.privateKeyFile
|
||||||
|
defer func() {
|
||||||
|
bootstrapArgs.sshSigningReusePrivateKey = savedReuse
|
||||||
|
bootstrapArgs.privateKeyFile = savedPrivKey
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Reuse flag set, no --private-key-file: bootstrapValidate
|
||||||
|
// would otherwise return "requires --private-key-file".
|
||||||
|
bootstrapArgs.sshSigningReusePrivateKey = true
|
||||||
|
bootstrapArgs.privateKeyFile = ""
|
||||||
|
|
||||||
|
err := tt.runE()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
@ -48,9 +49,10 @@ from the given directory or a single manifest file.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
type buildArtifactFlags struct {
|
type buildArtifactFlags struct {
|
||||||
output string
|
output string
|
||||||
path string
|
path string
|
||||||
ignorePaths []string
|
ignorePaths []string
|
||||||
|
resolveSymlinks bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
||||||
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,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
|
||||||
|
|
@ -85,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", false,
|
||||||
|
"Use in-memory filesystem during build.")
|
||||||
buildCmd.AddCommand(buildKsCmd)
|
buildCmd.AddCommand(buildKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,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,
|
||||||
|
|
@ -140,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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
@ -52,6 +54,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 (in-memory)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=true",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
||||||
|
|
@ -70,12 +78,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 (in-memory)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=true",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build with recursive",
|
name: "build with recursive",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive (in-memory)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=true",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
|
|
@ -145,6 +165,12 @@ spec:
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build podinfo (in-memory)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=true",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
||||||
|
|
@ -175,6 +201,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 (in-memory)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=true",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive in dry-run mode (in-memory)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=true --dry-run",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
|
|
@ -219,6 +257,104 @@ spec:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildKustomizationDefaultBuildsAbsolutePathOutsideCwd(t *testing.T) {
|
||||||
|
cwdDir, sourceDir, kustomizationFile := newOutsideCwdKustomization(t, "default", "")
|
||||||
|
|
||||||
|
restore := chdirForTest(t, cwdDir)
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
flag := buildKsCmd.Flags().Lookup("in-memory-build")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("missing in-memory-build flag")
|
||||||
|
}
|
||||||
|
defaultValue, err := strconv.ParseBool(flag.DefValue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
buildKsArgs.inMemoryBuild = defaultValue
|
||||||
|
|
||||||
|
output, err := executeCommand("build kustomization app --path " + sourceDir +
|
||||||
|
" --kustomization-file " + kustomizationFile +
|
||||||
|
" --namespace default --dry-run")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected build to succeed with default backend, got: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "name: outside-cwd") {
|
||||||
|
t.Fatalf("expected rendered ConfigMap in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chdirForTest(t *testing.T, dir string) func() {
|
||||||
|
t.Helper()
|
||||||
|
orig, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
if err := os.Chdir(orig); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOutsideCwdKustomization(t *testing.T, namespace, targetNamespace string) (string, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
parentDir := t.TempDir()
|
||||||
|
|
||||||
|
cwdDir := filepath.Join(parentDir, "cwd")
|
||||||
|
if err := os.MkdirAll(cwdDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceDir := filepath.Join(parentDir, "source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(sourceDir, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(sourceDir, "configmap.yaml"), []byte(`apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: outside-cwd
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetNamespaceField := ""
|
||||||
|
if targetNamespace != "" {
|
||||||
|
targetNamespaceField = "\n targetNamespace: " + targetNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
kustomizationFile := filepath.Join(parentDir, "app.yaml")
|
||||||
|
if err := os.WriteFile(kustomizationFile, []byte(`apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: app
|
||||||
|
namespace: `+namespace+`
|
||||||
|
spec:
|
||||||
|
interval: 5m
|
||||||
|
path: ./source
|
||||||
|
prune: true`+targetNamespaceField+`
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: app
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cwdDir, sourceDir, kustomizationFile
|
||||||
|
}
|
||||||
|
|
||||||
// TestBuildKustomizationPathNormalization verifies that absolute and complex
|
// TestBuildKustomizationPathNormalization verifies that absolute and complex
|
||||||
// paths are normalized to prevent path concatenation bugs (issue #5673).
|
// paths are normalized to prevent path concatenation bugs (issue #5673).
|
||||||
// Without normalization, paths could be duplicated like: /path/test/path/test/file
|
// Without normalization, paths could be duplicated like: /path/test/path/test/file
|
||||||
|
|
@ -241,6 +377,12 @@ func TestBuildKustomizationPathNormalization(t *testing.T) {
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with absolute path (in-memory)",
|
||||||
|
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=true",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build with complex relative path (parent dir)",
|
name: "build with complex relative path (parent dir)",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(helmReleaseArgs.dependsOn) > 0 {
|
if len(helmReleaseArgs.dependsOn) > 0 {
|
||||||
ls := utils.MakeDependsOn(helmReleaseArgs.dependsOn)
|
ls := meta.MakeDependsOn(helmReleaseArgs.dependsOn)
|
||||||
hrDependsOn := make([]helmv2.DependencyReference, 0, len(ls))
|
hrDependsOn := make([]helmv2.DependencyReference, 0, len(ls))
|
||||||
for _, d := range ls {
|
for _, d := range ls {
|
||||||
hrDependsOn = append(hrDependsOn, helmv2.DependencyReference{
|
hrDependsOn = append(hrDependsOn, helmv2.DependencyReference{
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import (
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||||
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -75,6 +76,8 @@ type imageUpdateFlags struct {
|
||||||
commitTemplate string
|
commitTemplate string
|
||||||
authorName string
|
authorName string
|
||||||
authorEmail string
|
authorEmail string
|
||||||
|
signingKeySecret string
|
||||||
|
signingKeyType string
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageUpdateArgs = imageUpdateFlags{}
|
var imageUpdateArgs = imageUpdateFlags{}
|
||||||
|
|
@ -89,6 +92,8 @@ func init() {
|
||||||
flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages")
|
flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages")
|
||||||
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
|
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
|
||||||
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
|
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
|
||||||
|
flags.StringVar(&imageUpdateArgs.signingKeySecret, "signing-key-secret", "", "name of the Secret containing the signing key referenced in spec.git.commit.signingKey")
|
||||||
|
flags.StringVar(&imageUpdateArgs.signingKeyType, "signing-key-type", "", "signing-key format: gpg or ssh (defaults to gpg when --signing-key-secret is set)")
|
||||||
|
|
||||||
createImageCmd.AddCommand(createImageUpdateCmd)
|
createImageCmd.AddCommand(createImageUpdateCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +117,15 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("the author email is required (--author-email)")
|
return fmt.Errorf("the author email is required (--author-email)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imageUpdateArgs.signingKeyType != "" && imageUpdateArgs.signingKeySecret == "" {
|
||||||
|
return fmt.Errorf("--signing-key-type requires --signing-key-secret")
|
||||||
|
}
|
||||||
|
if imageUpdateArgs.signingKeyType != "" &&
|
||||||
|
imageUpdateArgs.signingKeyType != string(autov1.SigningKeyTypeGPG) &&
|
||||||
|
imageUpdateArgs.signingKeyType != string(autov1.SigningKeyTypeSSH) {
|
||||||
|
return fmt.Errorf("--signing-key-type must be one of: gpg, ssh")
|
||||||
|
}
|
||||||
|
|
||||||
labels, err := parseLabels()
|
labels, err := parseLabels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -163,6 +177,13 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imageUpdateArgs.signingKeySecret != "" {
|
||||||
|
update.Spec.GitSpec.Commit.SigningKey = &autov1.SigningKey{
|
||||||
|
SecretRef: meta.LocalObjectReference{Name: imageUpdateArgs.signingKeySecret},
|
||||||
|
Type: autov1.SigningKeyType(imageUpdateArgs.signingKeyType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if createArgs.export {
|
if createArgs.export {
|
||||||
return printExport(exportImageUpdate(&update))
|
return printExport(exportImageUpdate(&update))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
cmd/flux/create_image_update_test.go
Normal file
62
cmd/flux/create_image_update_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCreateImageUpdate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no signing key",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --interval=1m0s --namespace=flux-system --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_image_update/no-signing.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signing secret without explicit type defaults to gpg",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=my-key --interval=1m0s --namespace=flux-system --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_image_update/signing-default-gpg.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh signing key",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=my-deploy-key --signing-key-type=ssh --interval=1m0s --namespace=flux-system --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_image_update/signing-ssh.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signing-key-type without secret errors",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-type=ssh --namespace=flux-system --export",
|
||||||
|
assert: assertError("--signing-key-type requires --signing-key-secret"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signing-key-type errors",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=k --signing-key-type=pgp --namespace=flux-system --export",
|
||||||
|
assert: assertError("--signing-key-type must be one of: gpg, ssh"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -172,7 +172,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(kustomizationArgs.dependsOn) > 0 {
|
if len(kustomizationArgs.dependsOn) > 0 {
|
||||||
ls := utils.MakeDependsOn(kustomizationArgs.dependsOn)
|
ls := meta.MakeDependsOn(kustomizationArgs.dependsOn)
|
||||||
ksDependsOn := make([]kustomizev1.DependencyReference, 0, len(ls))
|
ksDependsOn := make([]kustomizev1.DependencyReference, 0, len(ls))
|
||||||
for _, d := range ls {
|
for _, d := range ls {
|
||||||
ksDependsOn = append(ksDependsOn, kustomizev1.DependencyReference{
|
ksDependsOn = append(ksDependsOn, kustomizev1.DependencyReference{
|
||||||
|
|
|
||||||
|
|
@ -77,16 +77,18 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("secret ref is required")
|
return fmt.Errorf("secret ref is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
resources := []notificationv1.CrossNamespaceObjectReference{}
|
resources := []notificationv1.ReceiverResource{}
|
||||||
for _, resource := range receiverArgs.resources {
|
for _, resource := range receiverArgs.resources {
|
||||||
kind, name := utils.ParseObjectKindName(resource)
|
kind, name := utils.ParseObjectKindName(resource)
|
||||||
if kind == "" {
|
if kind == "" {
|
||||||
return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", resource)
|
return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
resources = append(resources, notificationv1.CrossNamespaceObjectReference{
|
resources = append(resources, notificationv1.ReceiverResource{
|
||||||
Kind: kind,
|
CrossNamespaceObjectReference: notificationv1.CrossNamespaceObjectReference{
|
||||||
Name: name,
|
Kind: kind,
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +115,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
Type: receiverArgs.receiverType.String(),
|
Type: receiverArgs.receiverType.String(),
|
||||||
Events: receiverArgs.events,
|
Events: receiverArgs.events,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
SecretRef: meta.LocalObjectReference{
|
SecretRef: &meta.LocalObjectReference{
|
||||||
Name: receiverArgs.secretRef,
|
Name: receiverArgs.secretRef,
|
||||||
},
|
},
|
||||||
Suspend: false,
|
Suspend: false,
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ func createSourceChartCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider := sourceChartArgs.verifyProvider.String(); provider != "" {
|
if provider := sourceChartArgs.verifyProvider.String(); provider != "" {
|
||||||
helmChart.Spec.Verify = &sourcev1.OCIRepositoryVerification{
|
helmChart.Spec.Verify = &sourcev1.HelmChartVerification{
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
}
|
}
|
||||||
if secretName := sourceChartArgs.verifySecretRef; secretName != "" {
|
if secretName := sourceChartArgs.verifySecretRef; secretName != "" {
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,12 @@ For private Git repositories, the basic authentication credentials are stored in
|
||||||
--username=username \
|
--username=username \
|
||||||
--password=password
|
--password=password
|
||||||
|
|
||||||
|
# Create a source for a Git repository using AWS CodeCommit with IAM credentials
|
||||||
|
flux create source git podinfo \
|
||||||
|
--url=https://git-codecommit.<region>.amazonaws.com/v1/repos/podinfo \
|
||||||
|
--branch=master \
|
||||||
|
--provider=aws
|
||||||
|
|
||||||
# Create a source for a Git repository using azure provider
|
# Create a source for a Git repository using azure provider
|
||||||
flux create source git podinfo \
|
flux create source git podinfo \
|
||||||
--url=https://dev.azure.com/foo/bar/_git/podinfo \
|
--url=https://dev.azure.com/foo/bar/_git/podinfo \
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ func TestCreateSourceGitExport(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "source with invalid provider",
|
name: "source with invalid provider",
|
||||||
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider dummy --branch=test --interval=1m0s --export",
|
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider dummy --branch=test --interval=1m0s --export",
|
||||||
assert: assertError("invalid argument \"dummy\" for \"--provider\" flag: source Git provider 'dummy' is not supported, must be one of: generic|azure|github"),
|
assert: assertError("invalid argument \"dummy\" for \"--provider\" flag: source Git provider 'dummy' is not supported, must be one of: generic|github|aws|azure"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "source with empty provider",
|
name: "source with empty provider",
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,16 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := url.Parse(sourceHelmArgs.url); err != nil {
|
helmURL, err := url.Parse(sourceHelmArgs.url)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("url parse failed: %w", err)
|
return fmt.Errorf("url parse failed: %w", err)
|
||||||
}
|
}
|
||||||
|
if helmURL.Scheme != "http" && helmURL.Scheme != "https" && helmURL.Scheme != sourcev1.HelmRepositoryTypeOCI {
|
||||||
|
return fmt.Errorf("url scheme '%s' not supported, can be: http, https and oci", helmURL.Scheme)
|
||||||
|
}
|
||||||
|
if helmURL.Host == "" {
|
||||||
|
return fmt.Errorf("url host is required")
|
||||||
|
}
|
||||||
|
|
||||||
helmRepository := &sourcev1.HelmRepository{
|
helmRepository := &sourcev1.HelmRepository{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|
@ -132,11 +139,7 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := url.Parse(sourceHelmArgs.url)
|
if helmURL.Scheme == sourcev1.HelmRepositoryTypeOCI {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse URL: %w", err)
|
|
||||||
}
|
|
||||||
if url.Scheme == sourcev1.HelmRepositoryTypeOCI {
|
|
||||||
helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI
|
helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI
|
||||||
helmRepository.Spec.Provider = sourceHelmArgs.ociProvider
|
helmRepository.Spec.Provider = sourceHelmArgs.ociProvider
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ func TestCreateSourceHelm(t *testing.T) {
|
||||||
resultFile: "name is required",
|
resultFile: "name is required",
|
||||||
assertFunc: "assertError",
|
assertFunc: "assertError",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported URL scheme",
|
||||||
|
args: "create source helm podinfo --url=git://example.com/charts --export",
|
||||||
|
resultFile: "url scheme 'git' not supported, can be: http, https and oci",
|
||||||
|
assertFunc: "assertError",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "OCI repo",
|
name: "OCI repo",
|
||||||
args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export",
|
args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
|
||||||
--verify-provider=cosign \
|
--verify-provider=cosign \
|
||||||
--verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \
|
--verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \
|
||||||
--verify-issuer="^https://token.actions.githubusercontent.com$"
|
--verify-issuer="^https://token.actions.githubusercontent.com$"
|
||||||
|
|
||||||
|
# Create an OCIRepository for a Helm chart layer
|
||||||
|
flux create source oci valkey-cluster \
|
||||||
|
--url=oci://example.com/charts/valkey \
|
||||||
|
--tag=0.11.6 \
|
||||||
|
--layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:copy
|
||||||
`,
|
`,
|
||||||
RunE: createSourceOCIRepositoryCmdRun,
|
RunE: createSourceOCIRepositoryCmdRun,
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +79,7 @@ type sourceOCIRepositoryFlags struct {
|
||||||
ignorePaths []string
|
ignorePaths []string
|
||||||
provider flags.SourceOCIProvider
|
provider flags.SourceOCIProvider
|
||||||
insecure bool
|
insecure bool
|
||||||
|
layerSelector string
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceOCIRepositoryArgs = newSourceOCIFlags()
|
var sourceOCIRepositoryArgs = newSourceOCIFlags()
|
||||||
|
|
@ -99,6 +106,7 @@ func init() {
|
||||||
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
|
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
|
||||||
createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
|
createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
|
||||||
createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
|
createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
|
||||||
|
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.layerSelector, "layer-selector", "", "the OCI artifact layer selector in the format '<media-type>:<operation>'")
|
||||||
|
|
||||||
createSourceCmd.AddCommand(createSourceOCIRepositoryCmd)
|
createSourceCmd.AddCommand(createSourceOCIRepositoryCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +122,11 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("--tag, --tag-semver or --digest is required")
|
return fmt.Errorf("--tag, --tag-semver or --digest is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layerSelector, err := parseLayerSelector(sourceOCIRepositoryArgs.layerSelector)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
sourceLabels, err := parseLabels()
|
sourceLabels, err := parseLabels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -152,6 +165,7 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
if tag := sourceOCIRepositoryArgs.tag; tag != "" {
|
if tag := sourceOCIRepositoryArgs.tag; tag != "" {
|
||||||
repository.Spec.Reference.Tag = tag
|
repository.Spec.Reference.Tag = tag
|
||||||
}
|
}
|
||||||
|
repository.Spec.LayerSelector = layerSelector
|
||||||
|
|
||||||
if createSourceArgs.fetchTimeout > 0 {
|
if createSourceArgs.fetchTimeout > 0 {
|
||||||
repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
|
repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
|
||||||
|
|
@ -234,6 +248,28 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseLayerSelector(selector string) (*sourcev1.OCILayerSelector, error) {
|
||||||
|
if selector == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, operation, found := strings.Cut(selector, ":")
|
||||||
|
if !found || mediaType == "" || operation == "" {
|
||||||
|
return nil, fmt.Errorf("invalid --layer-selector %q: must be in the format '<media-type>:<operation>'", selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch operation {
|
||||||
|
case sourcev1.OCILayerExtract, sourcev1.OCILayerCopy:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid --layer-selector %q: operation must be %q or %q", selector, sourcev1.OCILayerExtract, sourcev1.OCILayerCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sourcev1.OCILayerSelector{
|
||||||
|
MediaType: mediaType,
|
||||||
|
Operation: operation,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
|
func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
|
||||||
ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) {
|
ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) {
|
||||||
namespacedName := types.NamespacedName{
|
namespacedName := types.NamespacedName{
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,21 @@ func TestCreateSourceOCI(t *testing.T) {
|
||||||
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export",
|
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export",
|
||||||
assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"),
|
assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "export manifest with layer selector",
|
||||||
|
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:copy --export",
|
||||||
|
assertFunc: assertGoldenFile("./testdata/oci/export_with_layer_selector.golden"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid layer selector operation",
|
||||||
|
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:move --export",
|
||||||
|
assertFunc: assertError("invalid --layer-selector \"application/vnd.cncf.helm.chart.content.v1.tar+gzip:move\": operation must be \"extract\" or \"copy\""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid layer selector format",
|
||||||
|
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip --export",
|
||||||
|
assertFunc: assertError("invalid --layer-selector \"application/vnd.cncf.helm.chart.content.v1.tar+gzip\": must be in the format '<media-type>:<operation>'"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
|
||||||
|
|
@ -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", false,
|
||||||
|
"Use in-memory filesystem during build.")
|
||||||
|
diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false,
|
||||||
|
"Ignore Kustomization not found errors on the cluster when diffing.")
|
||||||
diffCmd.AddCommand(diffKsCmd)
|
diffCmd.AddCommand(diffKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -48,7 +49,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 +97,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 +139,153 @@ func TestDiffKustomization(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNotDeployed tests `flux diff ks` when the Kustomization
|
||||||
|
// CR does not exist in the cluster but is provided via --kustomization-file.
|
||||||
|
// Reproduces https://github.com/fluxcd/flux2/issues/5439
|
||||||
|
func TestDiffKustomizationNotDeployed(t *testing.T) {
|
||||||
|
// Use a dedicated namespace with NO setup() -- the Kustomization CR
|
||||||
|
// intentionally does not exist in the cluster.
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fails without --ignore-not-found",
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml",
|
||||||
|
assert: assertError("failed to get kustomization object: kustomizations.kustomize.toolkit.fluxcd.io \"podinfo\" not found"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "succeeds with --ignore-not-found and --kustomization-file",
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml " +
|
||||||
|
"--ignore-not-found",
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args + " -n " + tmpl["fluxns"],
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiffKustomizationDefaultBuildsAbsolutePathOutsideCwd(t *testing.T) {
|
||||||
|
fluxNS := allocateNamespace("flux-system")
|
||||||
|
targetNS := allocateNamespace("target")
|
||||||
|
setupTestNamespace(fluxNS, t)
|
||||||
|
setupTestNamespace(targetNS, t)
|
||||||
|
|
||||||
|
cwdDir, sourceDir, kustomizationFile := newOutsideCwdKustomization(t, fluxNS, targetNS)
|
||||||
|
|
||||||
|
restore := chdirForTest(t, cwdDir)
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
flag := diffKsCmd.Flags().Lookup("in-memory-build")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("missing in-memory-build flag")
|
||||||
|
}
|
||||||
|
defaultValue, err := strconv.ParseBool(flag.DefValue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
diffKsArgs.inMemoryBuild = defaultValue
|
||||||
|
|
||||||
|
output, err := executeCommand("diff kustomization app --path " + sourceDir +
|
||||||
|
" --kustomization-file " + kustomizationFile +
|
||||||
|
" --ignore-not-found --progress-bar=false -n " + fluxNS)
|
||||||
|
if isChangeError(err) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected diff to build with default backend, got: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "ConfigMap/"+targetNS+"/outside-cwd created") {
|
||||||
|
t.Fatalf("expected created ConfigMap in diff output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationTakeOwnership tests `flux diff ks` when taking ownership
|
||||||
|
// of existing resources on the cluster. A "pre-existing" configmap is applied
|
||||||
|
// to the cluster, and the kustomization contains a matching configmap; the
|
||||||
|
// diff should show the labels added by flux
|
||||||
|
func TestDiffKustomizationTakeOwnership(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
b, _ := build.NewBuilder("configmaps", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
|
||||||
|
resourceManager, err := b.Manager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-create the "existing" configmap in the cluster without Flux labels
|
||||||
|
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/existing-configmap.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization configmaps --path ./testdata/build-kustomization/configmaps --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-configmaps.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-taking-ownership.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNewNamespaceAndConfigmap runs `flux diff ks` when the
|
||||||
|
// kustomization creates a new namespace and resources inside it. The server-side
|
||||||
|
// dry-run cannot resolve resources in a namespace that doesn't exist yet,
|
||||||
|
// consistent with `kubectl diff --server-side` behavior.
|
||||||
|
func TestDiffKustomizationNewNamespaceAndConfigmap(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization new-namespace-and-configmap --path ./testdata/build-kustomization/new-namespace-and-configmap --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertError("ConfigMap/new-ns/app-config not found: namespaces \"new-ns\" not found"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNewNamespaceOnly runs `flux diff ks` when the
|
||||||
|
// kustomization creates only a new namespace. The diff should show the
|
||||||
|
// namespace as created.
|
||||||
|
func TestDiffKustomizationNewNamespaceOnly(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization new-namespace-only --path ./testdata/build-kustomization/new-namespace-only --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-namespace-only.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
||||||
buf, err := os.ReadFile(objectFile)
|
buf, err := os.ReadFile(objectFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -158,3 +306,36 @@ func createObjectFromFile(objectFile string, templateValues map[string]string, t
|
||||||
|
|
||||||
return clientObjects
|
return clientObjects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationDriftIgnoreRules tests `flux diff ks` with drift ignore
|
||||||
|
// rules. A service with a drifted port is pre-applied to the cluster, and the
|
||||||
|
// kustomization specifies driftIgnoreRules that ignore /spec/ports on Services.
|
||||||
|
// The diff should not show the service as drifted.
|
||||||
|
func TestDiffKustomizationDriftIgnoreRules(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
b, _ := build.NewBuilder("podinfo", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
|
||||||
|
resourceManager, err := b.Manager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-apply the drifted service (port 9899 instead of 9898) without Flux labels.
|
||||||
|
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/drifted-service-no-labels.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-drift-ignore.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drift-ignore.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
|
||||||
|
testEnv.DeleteObjectFile("./testdata/diff-kustomization/drifted-service-no-labels.yaml", tmpl, t)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ func init() {
|
||||||
getCmd.PersistentFlags().BoolVarP(&getArgs.noHeader, "no-header", "", false, "skip the header when printing the results")
|
getCmd.PersistentFlags().BoolVarP(&getArgs.noHeader, "no-header", "", false, "skip the header when printing the results")
|
||||||
getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.")
|
getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.")
|
||||||
getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "",
|
getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "",
|
||||||
"specify the status condition name and the desired state to filter the get result, e.g. ready=false")
|
"specify the status condition name and the desired state to filter the get result, e.g. ready=false or ready!=true")
|
||||||
getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "",
|
getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "",
|
||||||
"filter objects by label selector")
|
"filter objects by label selector")
|
||||||
rootCmd.AddCommand(getCmd)
|
rootCmd.AddCommand(getCmd)
|
||||||
|
|
@ -114,6 +114,11 @@ func statusMatches(conditionType, conditionStatus string, conditions []metav1.Co
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readyStatusMatches(conditionType, conditionStatus string) bool {
|
||||||
|
return strings.EqualFold(conditionType, meta.ReadyCondition) &&
|
||||||
|
strings.EqualFold(conditionStatus, string(metav1.ConditionTrue))
|
||||||
|
}
|
||||||
|
|
||||||
func nameColumns(item named, includeNamespace bool, includeKind bool) []string {
|
func nameColumns(item named, includeNamespace bool, includeKind bool) []string {
|
||||||
name := item.GetName()
|
name := item.GetName()
|
||||||
if includeKind {
|
if includeKind {
|
||||||
|
|
@ -207,6 +212,9 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if getAll && len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -228,20 +236,31 @@ func namespaceNameOrAny(allNamespaces bool, namespaceName string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) {
|
func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) {
|
||||||
noFilter := true
|
filter := func(i int) bool { return true }
|
||||||
var conditionType, conditionStatus string
|
var conditionType, conditionStatus string
|
||||||
if getArgs.statusSelector != "" {
|
if getArgs.statusSelector != "" {
|
||||||
parts := strings.SplitN(getArgs.statusSelector, "=", 2)
|
// Support both type=status (match) and type!=status (negated match).
|
||||||
|
// "!=" must be checked first since it also contains "=".
|
||||||
|
separator := "="
|
||||||
|
filter = func(i int) bool {
|
||||||
|
return list.statusSelectorMatches(i, conditionType, conditionStatus)
|
||||||
|
}
|
||||||
|
if strings.Contains(getArgs.statusSelector, "!=") {
|
||||||
|
separator = "!="
|
||||||
|
filter = func(i int) bool {
|
||||||
|
return !list.statusSelectorMatches(i, conditionType, conditionStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(getArgs.statusSelector, separator, 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return nil, fmt.Errorf("expected status selector in type=status format, but found: %s", getArgs.statusSelector)
|
return nil, fmt.Errorf("expected status selector in type=status or type!=status format, but found: %s", getArgs.statusSelector)
|
||||||
}
|
}
|
||||||
conditionType = parts[0]
|
conditionType = parts[0]
|
||||||
conditionStatus = parts[1]
|
conditionStatus = parts[1]
|
||||||
noFilter = false
|
|
||||||
}
|
}
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
for i := 0; i < list.len(); i++ {
|
for i := 0; i < list.len(); i++ {
|
||||||
if noFilter || list.statusSelectorMatches(i, conditionType, conditionStatus) {
|
if filter(i) {
|
||||||
row := list.summariseItem(i, getArgs.allNamespaces, getAll)
|
row := list.summariseItem(i, getArgs.allNamespaces, getAll)
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,5 +92,5 @@ func (s alertListAdapter) headers(includeNamespace bool) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||||
return false
|
return readyStatusMatches(conditionType, conditionStatus)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,5 +88,5 @@ func (s alertProviderListAdapter) headers(includeNamespace bool) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||||
return false
|
return readyStatusMatches(conditionType, conditionStatus)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ var getArtifactGeneratorCmd = &cobra.Command{
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
get := getCommand{
|
get := getCommand{
|
||||||
apiType: receiverType,
|
apiType: artifactGeneratorType,
|
||||||
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
|
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
|
||||||
funcMap: make(typeMap),
|
funcMap: make(typeMap),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,141 @@ func Test_GetCmd(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_GetCmdStatusSelector(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
testEnv.CreateObjectFile("./testdata/get/status_objects.yaml", tmpl, t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "equal status selector matches one",
|
||||||
|
args: "--status-selector Ready=True",
|
||||||
|
expected: "testdata/get/get_status_ready_true.golden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equal status selector matches false",
|
||||||
|
args: "--status-selector Ready=False",
|
||||||
|
expected: "testdata/get/get_status_ready_false.golden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-equal status selector matches all not-true",
|
||||||
|
args: "--status-selector Ready!=True",
|
||||||
|
expected: "testdata/get/get_status_ready_not_true.golden",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "get sources git " + tt.args + " -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenTemplateFile(tt.expected, nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetCmdStatusSelectorSyntheticReady(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
testEnv.CreateObjectFile("./testdata/get/notification_objects.yaml", tmpl, t)
|
||||||
|
|
||||||
|
commands := []string{
|
||||||
|
"get alerts",
|
||||||
|
"get alert-providers",
|
||||||
|
"get all",
|
||||||
|
}
|
||||||
|
for _, command := range commands {
|
||||||
|
t.Run(command, func(t *testing.T) {
|
||||||
|
unfilteredOutput, err := executeCommand(command + " -n " + tmpl["fluxns"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s failed: %v", command, err)
|
||||||
|
}
|
||||||
|
if unfilteredOutput == "" {
|
||||||
|
t.Fatalf("expected %s output for namespace with notification objects", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredOutput, err := executeCommand(command + " --status-selector Ready=True -n " + tmpl["fluxns"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s with Ready=True status selector failed: %v", command, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filteredOutput != unfilteredOutput {
|
||||||
|
t.Fatalf("expected Ready=True filtered output to match unfiltered output:\nfiltered:\n%s\nunfiltered:\n%s", filteredOutput, unfilteredOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetAllCmdStatusSelectorNoMatches(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
testEnv.CreateObjectFile("./testdata/get/status_objects.yaml", tmpl, t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "get all --status-selector foo=bar -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenValue(""),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetAllCmdStatusSelectorKustomizationOnlyMatches(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
testEnv.CreateObjectFile("./testdata/get/kustomization_only.yaml", tmpl, t)
|
||||||
|
|
||||||
|
unfilteredOutput, err := executeCommand("get all -n " + tmpl["fluxns"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get all failed: %v", err)
|
||||||
|
}
|
||||||
|
if unfilteredOutput == "" {
|
||||||
|
t.Fatal("expected get all output for namespace with one Kustomization")
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredOutput, err := executeCommand("get all --status-selector Ready=True -n " + tmpl["fluxns"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get all with matching status selector failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filteredOutput != unfilteredOutput {
|
||||||
|
t.Fatalf("expected filtered output to match unfiltered output:\nfiltered:\n%s\nunfiltered:\n%s", filteredOutput, unfilteredOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetAllCmdStatusSelectorKustomizationOnlyNoMatch(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
testEnv.CreateObjectFile("./testdata/get/kustomization_only.yaml", tmpl, t)
|
||||||
|
|
||||||
|
emptyNamespace := allocateNamespace("empty")
|
||||||
|
setupTestNamespace(emptyNamespace, t)
|
||||||
|
|
||||||
|
emptyOutput, err := executeCommand("get all -n " + emptyNamespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get all in empty namespace failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredOutput, err := executeCommand("get all --status-selector Ready=False -n " + tmpl["fluxns"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get all with non-matching status selector failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filteredOutput != emptyOutput {
|
||||||
|
t.Fatalf("expected filtered output to match empty namespace output:\nfiltered:\n%s\nempty namespace:\n%s", filteredOutput, emptyOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_GetCmdErrors(t *testing.T) {
|
func Test_GetCmdErrors(t *testing.T) {
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
"fluxns": allocateNamespace("flux-system"),
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
|
@ -84,6 +219,16 @@ func Test_GetCmdErrors(t *testing.T) {
|
||||||
args: "get helmrelease -n " + tmpl["fluxns"],
|
args: "get helmrelease -n " + tmpl["fluxns"],
|
||||||
assert: assertError(fmt.Sprintf("no HelmRelease objects found in \"%s\" namespace", tmpl["fluxns"])),
|
assert: assertError(fmt.Sprintf("no HelmRelease objects found in \"%s\" namespace", tmpl["fluxns"])),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "no artifact generators found in namespace",
|
||||||
|
args: "get artifact generators -n " + tmpl["fluxns"],
|
||||||
|
assert: assertError(fmt.Sprintf("no ArtifactGenerator objects found in \"%s\" namespace", tmpl["fluxns"])),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed status selector",
|
||||||
|
args: "get sources git --status-selector Ready -n " + tmpl["fluxns"],
|
||||||
|
assert: assertError("expected status selector in type=status or type!=status format, but found: Ready"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -116,10 +126,11 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
||||||
var logger = stderrLogger{stderr: os.Stderr}
|
var logger = stderrLogger{stderr: os.Stderr}
|
||||||
|
|
||||||
type rootFlags struct {
|
type rootFlags struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
verbose bool
|
verbose bool
|
||||||
pollInterval time.Duration
|
pollInterval time.Duration
|
||||||
defaults install.Options
|
nsFollowsKubeContext bool
|
||||||
|
defaults install.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestError is a custom error type that wraps an error returned by the flux api.
|
// RequestError is a custom error type that wraps an error returned by the flux api.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -186,6 +199,8 @@ func main() {
|
||||||
// logger, we configure it's logger to do nothing.
|
// logger, we configure it's logger to do nothing.
|
||||||
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
||||||
|
|
||||||
|
registerPlugins()
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
|
||||||
if err, ok := err.(*RequestError); ok {
|
if err, ok := err.(*RequestError); ok {
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
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 {
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -103,17 +103,18 @@ The command can read the credentials from '~/.docker/config.json' but they can a
|
||||||
}
|
}
|
||||||
|
|
||||||
type pushArtifactFlags struct {
|
type pushArtifactFlags struct {
|
||||||
path string
|
path string
|
||||||
source string
|
source string
|
||||||
revision string
|
revision string
|
||||||
creds string
|
creds string
|
||||||
provider flags.SourceOCIProvider
|
provider flags.SourceOCIProvider
|
||||||
ignorePaths []string
|
ignorePaths []string
|
||||||
annotations []string
|
annotations []string
|
||||||
output string
|
output string
|
||||||
debug bool
|
debug bool
|
||||||
reproducible bool
|
reproducible bool
|
||||||
insecure bool
|
insecure bool
|
||||||
|
resolveSymlinks bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushArtifactArgs = newPushArtifactFlags()
|
var pushArtifactArgs = newPushArtifactFlags()
|
||||||
|
|
@ -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, "=")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
8
cmd/flux/testdata/bootstrap/ed25519-encrypted.private
vendored
Normal file
8
cmd/flux/testdata/bootstrap/ed25519-encrypted.private
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDuUiEMA0
|
||||||
|
eUvKlmOsur2w9FAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIDF/w86ZQb5qmZtv
|
||||||
|
m1GvyLojiJdhmPtI9hJ9XPcP7HBoAAAAkG2cIOuSVdWInSC0P81ExiorUpiAGOjxxpgvKW
|
||||||
|
VYERfU1zU72Z/c9n1+z/IH5cJOhZ1vlqBO0rubl4s0KQFvY/LKcsc4N0x0uzpqrvcJP4tO
|
||||||
|
9VW8LrMnrPp7b6KVJPsbeSW1SBcUM24aCMzF4/wV03mN/Uqz30s+YgS9SU4Lz8AOkX58xX
|
||||||
|
yAV0gkmndIzZl+Og==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
7
cmd/flux/testdata/bootstrap/ed25519.private
vendored
Normal file
7
cmd/flux/testdata/bootstrap/ed25519.private
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCAAAAIjjSDmx40g5
|
||||||
|
sQAAAAtzc2gtZWQyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCA
|
||||||
|
AAAEAGpzSFuLkCNDD49+tysxSFFwdOsRnDj67vDT9bfwoSDhYOV20IV1IxchXW8vBe9HCT
|
||||||
|
h4SZKgMKnE2Rxs2mHd0IAAAABHRlc3QB
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
BIN
cmd/flux/testdata/bootstrap/gpg-encrypted.pgp
vendored
Normal file
BIN
cmd/flux/testdata/bootstrap/gpg-encrypted.pgp
vendored
Normal file
Binary file not shown.
BIN
cmd/flux/testdata/bootstrap/gpg.pgp
vendored
Normal file
BIN
cmd/flux/testdata/bootstrap/gpg.pgp
vendored
Normal file
Binary file not shown.
1
cmd/flux/testdata/bootstrap/malformed.private
vendored
Normal file
1
cmd/flux/testdata/bootstrap/malformed.private
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
not a real ssh key
|
||||||
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
|
||||||
19
cmd/flux/testdata/create_image_update/no-signing.yaml
vendored
Normal file
19
cmd/flux/testdata/create_image_update/no-signing.yaml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux@example.com
|
||||||
|
name: flux
|
||||||
|
interval: 1m0s
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
22
cmd/flux/testdata/create_image_update/signing-default-gpg.yaml
vendored
Normal file
22
cmd/flux/testdata/create_image_update/signing-default-gpg.yaml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux@example.com
|
||||||
|
name: flux
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-key
|
||||||
|
interval: 1m0s
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
23
cmd/flux/testdata/create_image_update/signing-ssh.yaml
vendored
Normal file
23
cmd/flux/testdata/create_image_update/signing-ssh.yaml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux@example.com
|
||||||
|
name: flux
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-deploy-key
|
||||||
|
type: ssh
|
||||||
|
interval: 1m0s
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
1
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
|
||||||
14
cmd/flux/testdata/diff-kustomization/diff-with-drift-ignore.golden
vendored
Normal file
14
cmd/flux/testdata/diff-kustomization/diff-with-drift-ignore.golden
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
â–ş Deployment/default/podinfo created
|
||||||
|
â–ş HorizontalPodAutoscaler/default/podinfo created
|
||||||
|
â–ş Service/default/podinfo drifted
|
||||||
|
|
||||||
|
metadata
|
||||||
|
+ one map entry added:
|
||||||
|
labels:
|
||||||
|
kustomize.toolkit.fluxcd.io/name: podinfo
|
||||||
|
kustomize.toolkit.fluxcd.io/namespace:
|
||||||
|
|
||||||
|
â–ş Secret/default/docker-secret created
|
||||||
|
â–ş Secret/default/secret-basic-auth-stringdata created
|
||||||
|
â–ş Secret/default/podinfo-token-77t89m9b67 created
|
||||||
|
â–ş Secret/default/db-user-pass-bkbd782d2c created
|
||||||
18
cmd/flux/testdata/diff-kustomization/drifted-service-no-labels.yaml
vendored
Normal file
18
cmd/flux/testdata/diff-kustomization/drifted-service-no-labels.yaml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: podinfo
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 9899
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: http
|
||||||
|
- port: 9999
|
||||||
|
targetPort: grpc
|
||||||
|
protocol: TCP
|
||||||
|
name: grpc
|
||||||
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
|
||||||
19
cmd/flux/testdata/diff-kustomization/flux-kustomization-drift-ignore.yaml
vendored
Normal file
19
cmd/flux/testdata/diff-kustomization/flux-kustomization-drift-ignore.yaml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
spec:
|
||||||
|
interval: 5m0s
|
||||||
|
path: ./kustomize
|
||||||
|
force: true
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: podinfo
|
||||||
|
targetNamespace: default
|
||||||
|
ignore:
|
||||||
|
- paths:
|
||||||
|
- "/spec/ports"
|
||||||
|
target:
|
||||||
|
kind: Service
|
||||||
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
|
||||||
4
cmd/flux/testdata/export/image-update.yaml
vendored
4
cmd/flux/testdata/export/image-update.yaml
vendored
|
|
@ -10,6 +10,10 @@ spec:
|
||||||
author:
|
author:
|
||||||
email: fluxcdbot@users.noreply.github.com
|
email: fluxcdbot@users.noreply.github.com
|
||||||
name: fluxcdbot
|
name: fluxcdbot
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-signing-key
|
||||||
|
type: ssh
|
||||||
interval: 1m0s
|
interval: 1m0s
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: GitRepository
|
kind: GitRepository
|
||||||
|
|
|
||||||
4
cmd/flux/testdata/export/objects.yaml
vendored
4
cmd/flux/testdata/export/objects.yaml
vendored
|
|
@ -67,6 +67,10 @@ spec:
|
||||||
email: fluxcdbot@users.noreply.github.com
|
email: fluxcdbot@users.noreply.github.com
|
||||||
name: fluxcdbot
|
name: fluxcdbot
|
||||||
messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
|
messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-signing-key
|
||||||
|
type: ssh
|
||||||
update:
|
update:
|
||||||
path: ./clusters/my-cluster
|
path: ./clusters/my-cluster
|
||||||
strategy: Setters
|
strategy: Setters
|
||||||
|
|
|
||||||
2
cmd/flux/testdata/get/get_status_ready_false.golden
vendored
Normal file
2
cmd/flux/testdata/get/get_status_ready_false.golden
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
NAME REVISION SUSPENDED READY MESSAGE
|
||||||
|
gr-failed False False failed to checkout and determine revision
|
||||||
3
cmd/flux/testdata/get/get_status_ready_not_true.golden
vendored
Normal file
3
cmd/flux/testdata/get/get_status_ready_not_true.golden
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
NAME REVISION SUSPENDED READY MESSAGE
|
||||||
|
gr-failed False False failed to checkout and determine revision
|
||||||
|
gr-unknown False Unknown reconciliation in progress
|
||||||
2
cmd/flux/testdata/get/get_status_ready_true.golden
vendored
Normal file
2
cmd/flux/testdata/get/get_status_ready_true.golden
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
NAME REVISION SUSPENDED READY MESSAGE
|
||||||
|
gr-ready main@sha1:696f056d False True Fetched revision: main@sha1:696f056d
|
||||||
26
cmd/flux/testdata/get/kustomization_only.yaml
vendored
Normal file
26
cmd/flux/testdata/get/kustomization_only.yaml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ .fluxns }}
|
||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
interval: 5m
|
||||||
|
path: ./clusters/production
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: podinfo
|
||||||
|
status:
|
||||||
|
lastAppliedRevision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f
|
||||||
|
conditions:
|
||||||
|
- lastTransitionTime: "2021-08-01T04:52:56Z"
|
||||||
|
message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f'
|
||||||
|
reason: ReconciliationSucceeded
|
||||||
|
status: "True"
|
||||||
|
type: Ready
|
||||||
31
cmd/flux/testdata/get/notification_objects.yaml
vendored
Normal file
31
cmd/flux/testdata/get/notification_objects.yaml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ .fluxns }}
|
||||||
|
---
|
||||||
|
apiVersion: notification.toolkit.fluxcd.io/v1beta3
|
||||||
|
kind: Provider
|
||||||
|
metadata:
|
||||||
|
name: slack
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
address: https://hooks.slack.com/services/mock
|
||||||
|
channel: alerts
|
||||||
|
type: slack
|
||||||
|
---
|
||||||
|
apiVersion: notification.toolkit.fluxcd.io/v1beta3
|
||||||
|
kind: Alert
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
eventSeverity: info
|
||||||
|
eventSources:
|
||||||
|
- kind: GitRepository
|
||||||
|
name: '*'
|
||||||
|
- kind: Kustomization
|
||||||
|
name: '*'
|
||||||
|
providerRef:
|
||||||
|
name: slack
|
||||||
|
summary: Slack notification
|
||||||
71
cmd/flux/testdata/get/status_objects.yaml
vendored
Normal file
71
cmd/flux/testdata/get/status_objects.yaml
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ .fluxns }}
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: gr-failed
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
secretRef:
|
||||||
|
name: flux-system
|
||||||
|
url: ssh://git@github.com/example/repo
|
||||||
|
interval: 5m
|
||||||
|
status:
|
||||||
|
conditions:
|
||||||
|
- lastTransitionTime: "2021-07-20T00:48:16Z"
|
||||||
|
message: 'failed to checkout and determine revision'
|
||||||
|
reason: GitOperationFailed
|
||||||
|
status: "False"
|
||||||
|
type: Ready
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: gr-ready
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
secretRef:
|
||||||
|
name: flux-system
|
||||||
|
url: ssh://git@github.com/example/repo
|
||||||
|
interval: 5m
|
||||||
|
status:
|
||||||
|
artifact:
|
||||||
|
lastUpdateTime: "2021-08-01T04:28:42Z"
|
||||||
|
revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f
|
||||||
|
path: "example"
|
||||||
|
url: "example"
|
||||||
|
digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f
|
||||||
|
conditions:
|
||||||
|
- lastTransitionTime: "2021-07-20T00:48:16Z"
|
||||||
|
message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f'
|
||||||
|
reason: GitOperationSucceed
|
||||||
|
status: "True"
|
||||||
|
type: Ready
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: gr-unknown
|
||||||
|
namespace: {{ .fluxns }}
|
||||||
|
spec:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
secretRef:
|
||||||
|
name: flux-system
|
||||||
|
url: ssh://git@github.com/example/repo
|
||||||
|
interval: 5m
|
||||||
|
status:
|
||||||
|
conditions:
|
||||||
|
- lastTransitionTime: "2021-07-20T00:48:16Z"
|
||||||
|
message: 'reconciliation in progress'
|
||||||
|
reason: Progressing
|
||||||
|
status: "Unknown"
|
||||||
|
type: Ready
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue