Compare commits
15 Commits
dependabot
...
v2.6.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bf37f6a56 | ||
|
|
8b219112c6 | ||
|
|
bda4c8187e | ||
|
|
3f281da738 | ||
|
|
963e99188c | ||
|
|
a48f81a66c | ||
|
|
55104dc188 | ||
|
|
e771ff28ab | ||
|
|
998fe11166 | ||
|
|
a6ac4c5b60 | ||
|
|
0d397d7d1f | ||
|
|
b73c7f7191 | ||
|
|
7aff0327ad | ||
|
|
3bb3ae3617 | ||
|
|
bf1af3c120 |
12
.github/labels.yaml
vendored
12
.github/labels.yaml
vendored
@@ -44,12 +44,12 @@
|
||||
description: Feature request proposals in the RFC format
|
||||
color: '#D621C3'
|
||||
aliases: ['area/RFC']
|
||||
- name: backport:release/v2.6.x
|
||||
description: To be backported to release/v2.6.x
|
||||
- name: backport:release/v2.3.x
|
||||
description: To be backported to release/v2.3.x
|
||||
color: '#ffd700'
|
||||
- name: backport:release/v2.7.x
|
||||
description: To be backported to release/v2.7.x
|
||||
- name: backport:release/v2.4.x
|
||||
description: To be backported to release/v2.4.x
|
||||
color: '#ffd700'
|
||||
- name: backport:release/v2.8.x
|
||||
description: To be backported to release/v2.8.x
|
||||
- name: backport:release/v2.5.x
|
||||
description: To be backported to release/v2.5.x
|
||||
color: '#ffd700'
|
||||
|
||||
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -23,7 +23,7 @@ amd when it finds a new controller version, the workflow performs the following
|
||||
- Updates the controller API package version in `go.mod`.
|
||||
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
||||
- Patches the controller Deployment version in `manifests/bases` overlay.
|
||||
- Opens a Pull Request against the checked out branch.
|
||||
- Opens a Pull Request against the `main` branch.
|
||||
- Triggers the e2e test suite to run for the opened PR.
|
||||
|
||||
|
||||
|
||||
2
.github/workflows/action.yaml
vendored
2
.github/workflows/action.yaml
vendored
@@ -24,6 +24,6 @@ jobs:
|
||||
name: action on ${{ matrix.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup flux
|
||||
uses: ./action
|
||||
|
||||
35
.github/workflows/backport.yaml
vendored
35
.github/workflows/backport.yaml
vendored
@@ -1,13 +1,34 @@
|
||||
name: backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed, labeled]
|
||||
permissions: read-all
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
pull-request:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # for reading and creating branches.
|
||||
pull-requests: write # for creating pull requests against release branches.
|
||||
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Create backport PRs
|
||||
uses: korthout/backport-action@436145e922f9561fc5ea157ff406f21af2d6b363 # v3.2.0
|
||||
# xref: https://github.com/korthout/backport-action#inputs
|
||||
with:
|
||||
# Use token to allow workflows to be triggered for the created PR
|
||||
github_token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
# Match labels with a pattern `backport:<target-branch>`
|
||||
label_pattern: '^backport:([^ ]+)$'
|
||||
# A bit shorter pull-request title than the default
|
||||
pull_title: '[${target_branch}] ${pull_title}'
|
||||
# Simpler PR description than default
|
||||
pull_description: |-
|
||||
Automated backport to `${target_branch}`, triggered by a label in #${pull_number}.
|
||||
|
||||
42
.github/workflows/conformance.yaml
vendored
42
.github/workflows/conformance.yaml
vendored
@@ -3,13 +3,13 @@ name: conformance
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ]
|
||||
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.26.x
|
||||
GO_VERSION: 1.24.x
|
||||
|
||||
jobs:
|
||||
conform-kubernetes:
|
||||
@@ -19,13 +19,13 @@ jobs:
|
||||
matrix:
|
||||
# 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
|
||||
KUBERNETES_VERSION: [1.33.0, 1.34.1, 1.35.0]
|
||||
KUBERNETES_VERSION: [1.31.5, 1.32.1, 1.33.0]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: |
|
||||
@@ -40,9 +40,9 @@ jobs:
|
||||
run: |
|
||||
make build
|
||||
- name: Setup Kubernetes
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||
with:
|
||||
version: v0.30.0
|
||||
version: v0.27.0
|
||||
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
|
||||
- name: Run e2e tests
|
||||
@@ -76,13 +76,13 @@ jobs:
|
||||
matrix:
|
||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||
# Available versions can be found with "replicated cluster versions"
|
||||
K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ]
|
||||
K3S_VERSION: [ 1.31.8, 1.32.4, 1.33.0 ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: |
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Build
|
||||
run: make build-dev
|
||||
- name: Create repository
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||
- name: Create cluster
|
||||
id: create-cluster
|
||||
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
kubernetes-distribution: "k3s"
|
||||
@@ -120,7 +120,8 @@ jobs:
|
||||
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
|
||||
- name: Run flux bootstrap
|
||||
run: |
|
||||
./bin/flux bootstrap git --manifests ./manifests/test/ \
|
||||
./bin/flux bootstrap git --manifests ./manifests/install/ \
|
||||
--components-extra=image-reflector-controller,image-automation-controller \
|
||||
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
||||
--branch=main \
|
||||
--path=clusters/k3s \
|
||||
@@ -150,7 +151,7 @@ jobs:
|
||||
kubectl delete ns flux-system --wait
|
||||
- name: Delete cluster
|
||||
if: ${{ always() }}
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
@@ -168,13 +169,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
||||
OPENSHIFT_VERSION: [ 4.20.0-okd ]
|
||||
OPENSHIFT_VERSION: [ 4.18.0-okd ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: |
|
||||
@@ -189,7 +190,7 @@ jobs:
|
||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Build
|
||||
run: make build-dev
|
||||
- name: Create repository
|
||||
@@ -199,7 +200,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||
- name: Create cluster
|
||||
id: create-cluster
|
||||
uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
kubernetes-distribution: "openshift"
|
||||
@@ -211,6 +212,7 @@ jobs:
|
||||
- name: Run flux bootstrap
|
||||
run: |
|
||||
./bin/flux bootstrap git --manifests ./manifests/openshift/ \
|
||||
--components-extra=image-reflector-controller,image-automation-controller \
|
||||
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
||||
--branch=main \
|
||||
--path=clusters/openshift \
|
||||
@@ -240,7 +242,7 @@ jobs:
|
||||
kubectl delete ns flux-system --wait
|
||||
- name: Delete cluster
|
||||
if: ${{ always() }}
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0
|
||||
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||
|
||||
49
.github/workflows/e2e-azure.yaml
vendored
49
.github/workflows/e2e-azure.yaml
vendored
@@ -22,21 +22,22 @@ permissions:
|
||||
|
||||
jobs:
|
||||
e2e-aks:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tests/integration
|
||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
# This job is currently disabled. Remove the false check when Azure subscription is enabled.
|
||||
if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: CheckoutD
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
go-version: 1.24.x
|
||||
cache-dependency-path: tests/integration/go.sum
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
- name: Setup Flux CLI
|
||||
run: make build
|
||||
working-directory: ./
|
||||
@@ -50,7 +51,7 @@ jobs:
|
||||
- name: Authenticate to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v1.4.6
|
||||
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.AZ_ARM_CLIENT_ID }}","clientSecret":"${{ secrets.AZ_ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZ_ARM_TENANT_ID }}"}'
|
||||
- name: Set dynamic variables in .env
|
||||
run: |
|
||||
cat > .env <<EOF
|
||||
@@ -60,35 +61,33 @@ jobs:
|
||||
run: cat .env
|
||||
- name: Run Azure e2e tests
|
||||
env:
|
||||
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
|
||||
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
|
||||
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
|
||||
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
|
||||
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
|
||||
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
|
||||
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
|
||||
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
|
||||
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
|
||||
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
|
||||
TF_VAR_azure_location: ${{ vars.TF_VAR_azure_location }}
|
||||
GITREPO_SSH_CONTENTS: ${{ secrets.GIT_SSH_IDENTITY }}
|
||||
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.GIT_SSH_IDENTITY_PUB }}
|
||||
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
|
||||
GITREPO_SSH_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_CONTENTS }}
|
||||
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_PUB_CONTENTS }}
|
||||
run: |
|
||||
source .env
|
||||
mkdir -p ./build/ssh
|
||||
cat <<EOF > build/ssh/key
|
||||
$GITREPO_SSH_CONTENTS
|
||||
EOF
|
||||
touch ./build/ssh/key
|
||||
echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key
|
||||
export GITREPO_SSH_PATH=build/ssh/key
|
||||
cat <<EOF > build/ssh/key.pub
|
||||
$GITREPO_SSH_PUB_CONTENTS
|
||||
EOF
|
||||
touch ./build/ssh/key.pub
|
||||
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
|
||||
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
|
||||
make test-azure
|
||||
- name: Ensure resource cleanup
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
|
||||
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
|
||||
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
|
||||
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
|
||||
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
|
||||
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
|
||||
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
|
||||
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
|
||||
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
|
||||
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
|
||||
TF_VAR_azure_location: ${{ vars.TF_VAR_azure_location }}
|
||||
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
|
||||
run: source .env && make destroy-azure
|
||||
|
||||
25
.github/workflows/e2e-bootstrap.yaml
vendored
25
.github/workflows/e2e-bootstrap.yaml
vendored
@@ -17,27 +17,27 @@ jobs:
|
||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
go-version: 1.24.x
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Setup Kubernetes
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||
with:
|
||||
version: v0.30.0
|
||||
version: v0.24.0
|
||||
cluster_name: kind
|
||||
# The versions below should target the newest Kubernetes version
|
||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||
kubectl_version: v1.33.0
|
||||
kubectl_version: v1.32.0
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Setup yq
|
||||
uses: fluxcd/pkg/actions/yq@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/yq@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Build
|
||||
run: make build-dev
|
||||
- name: Set outputs
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
|
||||
- name: bootstrap init
|
||||
run: |
|
||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||
--owner=fluxcd-testing \
|
||||
--image-pull-secret=ghcr-auth \
|
||||
--registry-creds=fluxcd:$GITHUB_TOKEN \
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
|
||||
- name: bootstrap no-op
|
||||
run: |
|
||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||
--owner=fluxcd-testing \
|
||||
--image-pull-secret=ghcr-auth \
|
||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: bootstrap customize
|
||||
run: |
|
||||
make setup-bootstrap-patch
|
||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||
--owner=fluxcd-testing \
|
||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||
--branch=main \
|
||||
@@ -98,14 +98,13 @@ jobs:
|
||||
- name: test image automation
|
||||
run: |
|
||||
make setup-image-automation
|
||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
||||
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||
--owner=fluxcd-testing \
|
||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||
--branch=main \
|
||||
--path=test-cluster \
|
||||
--read-write-key
|
||||
./bin/flux reconcile image repository podinfo
|
||||
./bin/flux reconcile image policy podinfo
|
||||
./bin/flux reconcile image update flux-system
|
||||
./bin/flux get images all
|
||||
./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system
|
||||
|
||||
20
.github/workflows/e2e-gcp.yaml
vendored
20
.github/workflows/e2e-gcp.yaml
vendored
@@ -22,21 +22,21 @@ permissions:
|
||||
|
||||
jobs:
|
||||
e2e-gcp:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tests/integration
|
||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
go-version: 1.24.x
|
||||
cache-dependency-path: tests/integration/go.sum
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
- name: Setup Flux CLI
|
||||
run: make build
|
||||
working-directory: ./
|
||||
@@ -48,19 +48,19 @@ jobs:
|
||||
env:
|
||||
SOPS_VER: 3.7.1
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
|
||||
id: 'auth'
|
||||
with:
|
||||
credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}'
|
||||
token_format: 'access_token'
|
||||
- name: Setup gcloud
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- name: Log into us-central1-docker.pkg.dev
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: us-central1-docker.pkg.dev
|
||||
username: oauth2accesstoken
|
||||
|
||||
21
.github/workflows/e2e.yaml
vendored
21
.github/workflows/e2e.yaml
vendored
@@ -23,30 +23,30 @@ jobs:
|
||||
- 5000:5000
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
go-version: 1.24.x
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Setup Kubernetes
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||
with:
|
||||
version: v0.30.0
|
||||
version: v0.24.0
|
||||
cluster_name: kind
|
||||
wait: 5s
|
||||
config: .github/kind/config.yaml # disable KIND-net
|
||||
# The versions below should target the oldest supported Kubernetes version
|
||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||
kubectl_version: v1.33.0
|
||||
node_image: ghcr.io/fluxcd/kindest/node:v1.31.5-amd64
|
||||
kubectl_version: v1.32.0
|
||||
- name: Setup Calico for network policy
|
||||
run: |
|
||||
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- name: Run e2e tests
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
./bin/flux check --pre
|
||||
- name: flux install --manifests
|
||||
run: |
|
||||
./bin/flux install --manifests ./manifests/test/
|
||||
./bin/flux install --manifests ./manifests/install/
|
||||
- name: flux create secret
|
||||
run: |
|
||||
./bin/flux create secret git git-ssh-test \
|
||||
@@ -238,9 +238,6 @@ jobs:
|
||||
- name: flux check
|
||||
run: |
|
||||
./bin/flux check
|
||||
- name: flux migrate
|
||||
run: |
|
||||
./bin/flux migrate
|
||||
- name: flux version
|
||||
run: |
|
||||
./bin/flux version
|
||||
|
||||
8
.github/workflows/ossf.yaml
vendored
8
.github/workflows/ossf.yaml
vendored
@@ -19,21 +19,21 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_results: true
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
- name: Upload SARIF results
|
||||
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
44
.github/workflows/release.yaml
vendored
44
.github/workflows/release.yaml
vendored
@@ -13,44 +13,40 @@ jobs:
|
||||
hashes: ${{ steps.slsa.outputs.hashes }}
|
||||
image_url: ${{ steps.slsa.outputs.image_url }}
|
||||
image_digest: ${{ steps.slsa.outputs.image_digest }}
|
||||
runs-on:
|
||||
group: "Default Larger Runners"
|
||||
labels: ubuntu-latest-16-cores
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # needed to write releases
|
||||
id-token: write # needed for keyless signing
|
||||
packages: write # needed for ghcr access
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Unshallow
|
||||
run: git fetch --prune --unshallow
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
go-version: 1.24.x
|
||||
cache: false
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- name: Setup Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- name: Setup Syft
|
||||
uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
|
||||
uses: anchore/sbom-action/download-syft@9f7302141466aa6482940f15371237e9d9f4c34a # v0.19.0
|
||||
- name: Setup Cosign
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
with:
|
||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||
@@ -63,7 +59,7 @@ jobs:
|
||||
run: |
|
||||
kustomize build manifests/crds > all-crds.yaml
|
||||
- name: Generate OpenAPI JSON schemas from CRDs
|
||||
uses: fluxcd/pkg/actions/crdjsonschema@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/crdjsonschema@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
with:
|
||||
crd: all-crds.yaml
|
||||
output: schemas
|
||||
@@ -72,7 +68,7 @@ jobs:
|
||||
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
||||
- name: Run GoReleaser
|
||||
id: run-goreleaser
|
||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip=validate
|
||||
@@ -103,26 +99,24 @@ jobs:
|
||||
id-token: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Kustomize
|
||||
uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main
|
||||
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||
- name: Setup Flux CLI
|
||||
uses: ./action/
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Prepare
|
||||
id: prep
|
||||
run: |
|
||||
VERSION=$(flux version --client | awk '{ print $NF }')
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: fluxcdbot
|
||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||
@@ -150,9 +144,7 @@ jobs:
|
||||
--path="./flux-system" \
|
||||
--source=${{ github.repositoryUrl }} \
|
||||
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
||||
- uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
with:
|
||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||
- uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
- name: Sign manifests
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: 1
|
||||
|
||||
50
.github/workflows/scan.yaml
vendored
50
.github/workflows/scan.yaml
vendored
@@ -1,4 +1,5 @@
|
||||
name: scan
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -7,13 +8,46 @@ on:
|
||||
branches: [ 'main', 'release/**' ]
|
||||
schedule:
|
||||
- cron: '18 10 * * 3'
|
||||
permissions: read-all
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
scan-fossa:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Run FOSSA scan and upload build data
|
||||
uses: fossa-contrib/fossa-action@3d2ef181b1820d6dcd1972f86a767d18167fa19b # v3.0.1
|
||||
with:
|
||||
# FOSSA Push-Only API Token
|
||||
fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
scan-codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # for reading the repository code.
|
||||
security-events: write # for uploading the CodeQL analysis results.
|
||||
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
||||
security-events: write
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
with:
|
||||
languages: go
|
||||
# xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# xref: https://codeql.github.com/codeql-query-help/go/
|
||||
queries: security-and-quality
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
|
||||
25
.github/workflows/sync-labels.yaml
vendored
25
.github/workflows/sync-labels.yaml
vendored
@@ -6,12 +6,23 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- .github/labels.yaml
|
||||
permissions: read-all
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-labels:
|
||||
labels:
|
||||
name: Run sync
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # for reading the labels file.
|
||||
issues: write # for creating and updating labels.
|
||||
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
|
||||
with:
|
||||
# Configuration file
|
||||
config-file: |
|
||||
https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml
|
||||
.github/labels.yaml
|
||||
# Strictly declarative
|
||||
delete-other-labels: true
|
||||
|
||||
32
.github/workflows/update.yaml
vendored
32
.github/workflows/update.yaml
vendored
@@ -2,6 +2,8 @@ name: update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
@@ -16,37 +18,24 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
go-version: 1.24.x
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Update component versions
|
||||
id: update
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_BODY=$(mktemp)
|
||||
|
||||
bump_version() {
|
||||
local LATEST_VERSION=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/fluxcd/$1/releases | jq -r 'sort_by(.published_at) | .[-1] | .tag_name')
|
||||
|
||||
if [[ "$LATEST_VERSION" == *"-rc"* ]]; then
|
||||
echo "Skipping release candidate version for $1: $LATEST_VERSION"
|
||||
return
|
||||
fi
|
||||
|
||||
local LATEST_VERSION=$(curl -s https://api.github.com/repos/fluxcd/$1/releases | jq -r 'sort_by(.published_at) | .[-1] | .tag_name')
|
||||
local CTRL_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p;n" manifests/bases/$1/kustomization.yaml)
|
||||
local CRD_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p" manifests/crds/kustomization.yaml)
|
||||
|
||||
local API_PKG="github.com/fluxcd/$1/api"
|
||||
if [[ "$1" == "source-watcher" ]]; then
|
||||
API_PKG="github.com/fluxcd/$1/api/v2"
|
||||
fi
|
||||
local MOD_VERSION=$(go list -m -f '{{ .Version }}' "$API_PKG")
|
||||
local MOD_VERSION=$(go list -m -f '{{ .Version }}' "github.com/fluxcd/$1/api")
|
||||
|
||||
local changed=false
|
||||
|
||||
@@ -61,7 +50,7 @@ jobs:
|
||||
fi
|
||||
|
||||
if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then
|
||||
go mod edit -require="$API_PKG@${LATEST_VERSION}"
|
||||
go mod edit -require="github.com/fluxcd/$1/api@${LATEST_VERSION}"
|
||||
make tidy
|
||||
changed=true
|
||||
fi
|
||||
@@ -80,7 +69,6 @@ jobs:
|
||||
bump_version notification-controller
|
||||
bump_version image-reflector-controller
|
||||
bump_version image-automation-controller
|
||||
bump_version source-watcher
|
||||
|
||||
# diff change
|
||||
git diff
|
||||
@@ -96,7 +84,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
@@ -106,7 +94,7 @@ jobs:
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
||||
signoff: true
|
||||
branch: update-components-${{ github.ref_name }}
|
||||
branch: update-components
|
||||
title: Update toolkit components
|
||||
body: |
|
||||
${{ steps.update.outputs.pr_body }}
|
||||
|
||||
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
@@ -1,13 +0,0 @@
|
||||
name: upgrade-fluxcd-pkg
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upgrade-fluxcd-pkg:
|
||||
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
|
||||
secrets:
|
||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
@@ -88,6 +88,22 @@ brews:
|
||||
generate_completions_from_executable(bin/"flux", "completion")
|
||||
test: |
|
||||
system "#{bin}/flux --version"
|
||||
publishers:
|
||||
- name: aur-pkg-bin
|
||||
env:
|
||||
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
||||
cmd: |
|
||||
.github/aur/flux-bin/publish.sh {{ .Version }}
|
||||
- name: aur-pkg-scm
|
||||
env:
|
||||
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
||||
cmd: |
|
||||
.github/aur/flux-scm/publish.sh {{ .Version }}
|
||||
- name: aur-pkg-go
|
||||
env:
|
||||
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
||||
cmd: |
|
||||
.github/aur/flux-go/publish.sh {{ .Version }}
|
||||
dockers:
|
||||
- image_templates:
|
||||
- 'fluxcd/flux-cli:{{ .Tag }}-amd64'
|
||||
|
||||
151
AGENTS.md
151
AGENTS.md
@@ -1,151 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI coding assistants working in `fluxcd/flux2`. Read this file before making changes.
|
||||
|
||||
## Contribution workflow for AI agents
|
||||
|
||||
These rules come from [`fluxcd/flux2/CONTRIBUTING.md`](https://github.com/fluxcd/flux2/blob/main/CONTRIBUTING.md) and apply to every Flux repository.
|
||||
|
||||
- **Do not add `Signed-off-by` or `Co-authored-by` trailers with your agent name.** Only a human can legally certify the DCO.
|
||||
- **Disclose AI assistance** with an `Assisted-by` trailer naming your agent and model:
|
||||
```sh
|
||||
git commit -s -m "Add support for X" --trailer "Assisted-by: <agent-name>/<model-id>"
|
||||
```
|
||||
The `-s` flag adds the human's `Signed-off-by` from their git config — do not remove it.
|
||||
- **Commit message format:** Subject in imperative mood ("Add feature X" instead of "Adding feature X"), capitalized, no trailing period, ≤50 characters. Body wrapped at 72 columns, explaining what and why. No `@mentions` or `#123` issue references in the commit — put those in the PR description.
|
||||
- **Trim verbiage:** in PR descriptions, commit messages, and code comments. No marketing prose, no restating the diff, no emojis.
|
||||
- **Rebase, don't merge:** Never merge `main` into the feature branch; rebase onto the latest `main` and push with `--force-with-lease`. Squash before merge when asked.
|
||||
- **Pre-PR gate:** `make tidy fmt vet && make test` must pass and the working tree must be clean.
|
||||
- **Flux is GA:** Backward compatibility is mandatory. Breaking changes to CLI flags, output format, or behavior will be rejected. Design additive changes.
|
||||
- **Copyright:** All new `.go` files must begin with the header from `cmd/flux/main.go` (Apache 2.0). Update the year to the current year when copying.
|
||||
- **Tests:** New features, improvements and fixes must have test coverage. Add unit tests in `cmd/flux/*_test.go` tagged `//go:build unit`. Follow the existing `cmdTestCase` + golden file patterns. Run tests locally before pushing.
|
||||
|
||||
## Code quality
|
||||
|
||||
Before submitting code, review your changes for the following:
|
||||
|
||||
- **No secrets in logs or output.** Never surface auth tokens, passwords, deploy keys, or credential URLs in error messages, log lines, or CLI output. Bootstrap and source-secret commands handle sensitive material — take extra care.
|
||||
- **No unchecked I/O.** Close HTTP response bodies, file handles, and tar readers in `defer` statements. Check and propagate errors from I/O operations.
|
||||
- **No path traversal.** Validate and sanitize file paths extracted from archives or user input. Never `filepath.Join` with untrusted components without validation.
|
||||
- **No command injection.** Do not shell out via `os/exec` for git, helm, or kustomize operations. Use the Go libraries already in use (`fluxcd/pkg/git`, `fluxcd/pkg/kustomize`, `fluxcd/pkg/ssa`).
|
||||
- **No hardcoded defaults for security settings.** TLS verification must remain enabled by default. Git auth settings come from user-provided secrets.
|
||||
- **Error handling.** Wrap errors with `%w` for chain inspection. Do not swallow errors silently. CLI errors must be actionable — tell the user what went wrong and how to fix it without leaking internal state.
|
||||
- **Resource cleanup.** Ensure temporary files and directories (manifest staging, downloaded tarballs) are cleaned up on all code paths (success and error). Use `defer` and `t.TempDir()` in tests.
|
||||
- **No panics.** Never use `panic` in runtime code paths. Return errors and let the CLI handle them gracefully.
|
||||
- **Output discipline.** Machine-readable data (tables, YAML, JSON) goes to stdout via `rootCmd.OutOrStdout()`. Human-readable status messages go to stderr via the `stderrLogger`.
|
||||
- **Minimal surface.** Keep new exported APIs in `pkg/` to the minimum needed. Every export is a backward-compatibility commitment.
|
||||
|
||||
## Project overview
|
||||
|
||||
flux2 is the Flux CLI (`flux` command) and distribution repository. It is **not** a controller — it consumes CRD APIs from six independent controller repos (source-controller, kustomize-controller, helm-controller, notification-controller, image-reflector-controller, image-automation-controller). It serves two purposes:
|
||||
|
||||
1. **CLI tool** — a Cobra-based binary that installs Flux onto Kubernetes clusters, bootstraps GitOps pipelines, and manages all Flux CRD objects (create, get, export, reconcile, suspend, resume, delete, diff, build, etc.).
|
||||
2. **Distribution hub** — it bundles the Kustomize manifests for all Flux controllers and releases them as `manifests.tar.gz` on GitHub. Those manifests are also compiled into the binary itself via `//go:embed`.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `cmd/flux/` — all CLI source. Single `main` package with one file per command or resource type. `main.go` defines the root cobra command with global flags. `manifests.embed.go` embeds the generated controller manifests via `//go:embed`.
|
||||
- `internal/build/` — `flux build kustomization` logic (kustomize-based diff/build, SOPS secret masking).
|
||||
- `internal/flags/` — custom `pflag.Value` types providing enum validation (e.g. `LogLevel`, `ECDSACurve`, `RSAKeyBits`, `PublicKeyAlgorithm`, `DecryptionProvider`).
|
||||
- `internal/tree/` — tree-printing helper for `flux tree kustomization`.
|
||||
- `internal/utils/` — shared helpers: `KubeClient`, `KubeConfig`, `NewScheme` (registers all controller API groups), `Apply` (SSA-based two-phase apply), `ExecKubectlCommand`, `ValidateComponents`.
|
||||
- `pkg/bootstrap/` — bootstrap orchestration: `Run()`, `PlainGitBootstrapper`, `ProviderBootstrapper`. `provider/` has the git provider factory (GitHub, GitLab, Gitea, Bitbucket).
|
||||
- `pkg/log/` — `Logger` interface (`Actionf`, `Generatef`, `Waitingf`, `Successf`, `Warningf`, `Failuref`).
|
||||
- `pkg/manifestgen/` — manifest generation for install, sync, kustomization, and source secrets.
|
||||
- `pkg/printers/` — specialized printers `TablePrinter` and `DyffPrinter`.
|
||||
- `pkg/status/` — `StatusChecker` using `fluxcd/cli-utils` kstatus polling.
|
||||
- `pkg/uninstall/` — `flux uninstall` logic.
|
||||
- `manifests/` — Kustomize bases per controller, RBAC, network policies, CRD references, and `scripts/bundle.sh` which runs `kustomize build` to generate `cmd/flux/manifests/`.
|
||||
- `tests/integration/` — cloud e2e tests (Azure/GCP) with their own `go.mod` and Terraform infrastructure.
|
||||
- `rfcs/` — Request for Comments documents for major design proposals and changes.
|
||||
|
||||
## CLI architecture
|
||||
|
||||
Commands are Cobra-based, organized as parent + per-resource children:
|
||||
- Group files (`create.go`, `get.go`, `reconcile.go`, etc.) register the parent subcommand.
|
||||
- Per-resource files (`create_kustomization.go`, `get_helmrelease.go`, etc.) register children.
|
||||
|
||||
Core interfaces in `cmd/flux/` enable generic command implementations:
|
||||
- `adapter` / `copyable` / `listAdapter` — wrap controller API types for generic CRUD.
|
||||
- `reconcilable` — annotate-and-poll pattern for triggering reconciliation.
|
||||
- `summarisable` — generic table output for `get` commands.
|
||||
|
||||
Each resource type (e.g. `kustomizationAdapter` in `kustomization.go`) wraps the controller API type and implements these interfaces. Follow this pattern when adding new resource support.
|
||||
|
||||
Commands interact with the Kubernetes API via `internal/utils.KubeClient()` → `client.WithWatch`. `internal/utils.NewScheme()` registers all six controller API groups plus core k8s types. `internal/utils.Apply()` implements SSA-based two-phase apply (CRDs/Namespaces first, then remaining objects).
|
||||
|
||||
## Manifest pipeline
|
||||
|
||||
1. `manifests/bases/<controller>/` contains a Kustomize base per controller referencing the controller's GitHub release for CRDs and deployment manifests.
|
||||
2. `manifests/install/kustomization.yaml` assembles all bases plus RBAC and policies.
|
||||
3. `manifests/scripts/bundle.sh` runs `kustomize build` on each base, writing output to `cmd/flux/manifests/` (not checked in — generated).
|
||||
4. The Makefile `$(EMBEDDED_MANIFESTS_TARGET)` runs `bundle.sh` and creates a sentinel file `cmd/flux/.manifests.done`.
|
||||
5. `cmd/flux/manifests.embed.go` uses `//go:embed manifests/*.yaml` to compile everything into the binary.
|
||||
|
||||
When modifying `manifests/`, always run `make build` and verify the generated output before committing. Never hand-edit files under `cmd/flux/manifests/`.
|
||||
|
||||
## Build, test, lint
|
||||
|
||||
All targets in the root `Makefile`. Go version tracks `go.mod`.
|
||||
|
||||
- `make tidy` — tidy the root module and `tests/integration/`.
|
||||
- `make fmt` / `make vet` — run in the root module.
|
||||
- `make build` — builds `bin/flux` (CGO disabled, version injected via ldflags). Depends on embedded manifests being generated.
|
||||
- `make build-dev` — builds with `DEV_VERSION`.
|
||||
- `make install` / `make install-dev` — `go install` or copy to `/usr/local/bin`.
|
||||
- `make test` — unit tests with envtest: runs `tidy fmt vet install-envtest`, then `go test ./... -coverprofile cover.out --tags=unit $(TEST_ARGS)`.
|
||||
- `make e2e` — e2e tests against a live cluster: `go test ./cmd/flux/... --tags=e2e -v -failfast`.
|
||||
- `make test-with-kind` — sets up a kind cluster, runs e2e, tears it down.
|
||||
- `make install-envtest` — downloads `setup-envtest` and fetches k8s binaries into `testbin/`.
|
||||
|
||||
Run a single test: `make test TEST_ARGS='-run TestCreate -v'`.
|
||||
|
||||
## Codegen and generated files
|
||||
|
||||
Check `go.mod` and the `Makefile` for current dependency and tool versions. The main codegen pipeline is the manifest bundle:
|
||||
|
||||
```sh
|
||||
./manifests/scripts/bundle.sh
|
||||
```
|
||||
|
||||
Generated files (never hand-edit):
|
||||
|
||||
- `cmd/flux/manifests/*.yaml` — generated by `bundle.sh` from `manifests/` sources.
|
||||
- `cmd/flux/.manifests.done` — sentinel file tracking bundle state.
|
||||
|
||||
Bump `fluxcd/pkg/*` and controller `api` modules as a set. Run `make tidy` after any bump.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Standard `gofmt`. All exported names need doc comments.
|
||||
- **Command pattern:** follow the existing group-parent + per-resource-child cobra structure. New resources need an adapter type implementing `adapter`, `copyable`, and the relevant command interfaces (`reconcilable`, `summarisable`, etc.).
|
||||
- **Output:** stderr for human status messages via `stderrLogger` (Unicode symbols: `►` action, `✔` success, `✗` failure, `◎` waiting, `⚠️` warning, `✚` generate). Stdout for machine-readable data (tables, YAML, JSON) via `rootCmd.OutOrStdout()`.
|
||||
- **Global flags:** kubeconfig flags come from `k8s.io/cli-runtime/pkg/genericclioptions.ConfigFlags`. Client tuning comes from `fluxcd/pkg/runtime/client.Options`. `FLUX_SYSTEM_NAMESPACE` env var overrides the default namespace.
|
||||
- **SSA apply:** always use `internal/utils.Apply()` (two-phase: CRDs/Namespaces first, then rest). Do not apply manifests directly via the k8s client.
|
||||
- **Reconcile triggering:** patch `meta.ReconcileRequestAnnotation` with a timestamp, then poll with `kstatus.Compute()` until ready. See `reconcile.go`.
|
||||
- **Error handling:** return errors from `RunE`. Use `*RequestError` with exit codes for actionable CLI errors. Exit code 1 = warning, anything else = failure.
|
||||
- **Flags:** use `internal/flags/` custom `pflag.Value` types for enum flags (providers, algorithms, sources). Add new enum types there.
|
||||
|
||||
## Testing
|
||||
|
||||
Three test suites with build tags:
|
||||
|
||||
- **Unit** (`//go:build unit`): lives in `cmd/flux/*_test.go`. Uses `controller-runtime/envtest` for an in-process fake k8s API. CRDs are loaded from `cmd/flux/manifests/` (embedded manifests). Pattern: `cmdTestCase{args: "...", assert: assertGoldenFile("testdata/...")}`. The `executeCommand()` helper captures stdout.
|
||||
- **E2e** (`//go:build e2e`): lives in `cmd/flux/*_test.go`. Requires a live cluster via `TEST_KUBECONFIG`. `TestMain` runs `flux install` for setup and teardown.
|
||||
- **Integration** (`//go:build integration`): lives in `tests/integration/` with its own `go.mod`. Uses Terraform-provisioned cloud clusters.
|
||||
|
||||
Golden files live in `cmd/flux/testdata/`. Update them with `go test ./cmd/flux/... --tags=unit -update`.
|
||||
|
||||
Run a single unit test: `make test TEST_ARGS='-run TestInstall -v'`.
|
||||
|
||||
## Gotchas and non-obvious rules
|
||||
|
||||
- The `cmd/flux/manifests/` directory is **generated, not checked in**. It is created by `manifests/scripts/bundle.sh` and embedded into the binary. `make build` and `make test` both trigger the bundle if the sentinel file is stale.
|
||||
- `kustomize` must be on `PATH` for `bundle.sh` to work. If you see "command not found" errors during build, install kustomize.
|
||||
- `internal/utils.NewScheme()` registers all six controller API groups. Adding support for a new CRD type means updating the scheme registration there.
|
||||
- The `VERSION` constant is injected via `-ldflags` at build time. In dev builds it defaults to `0.0.0-dev.0`. The embedded manifest version check (`isEmbeddedVersion`) determines whether `flux install` uses compiled-in manifests or downloads from GitHub.
|
||||
- `resetCmdArgs()` in tests is critical — Cobra persists flag state between test runs. Every test case must reset to avoid pollution.
|
||||
- `executeCommand()` captures stdout only. Stderr output (from `stderrLogger`) is not captured in test assertions. If your command's output goes to the wrong stream, tests will silently pass with empty golden files.
|
||||
- The `adapter` / `listAdapter` interfaces use type assertions internally. If you add a new resource type and forget to implement an interface method, you'll get a runtime panic in the generic command handler, not a compile error. Add interface compliance checks (`var _ reconcilable = ...`).
|
||||
- Bootstrap commands create real Git commits and push to real repos. E2e tests for bootstrap need careful cleanup. Do not add bootstrap e2e tests without a corresponding teardown.
|
||||
- `pkg/` packages are importable by external consumers (e.g. Terraform provider, other tools). Treat their exported surface as public API.
|
||||
235
CONTRIBUTING.md
235
CONTRIBUTING.md
@@ -1,129 +1,154 @@
|
||||
# Contributing
|
||||
|
||||
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and accepts contributions via GitHub pull requests.
|
||||
This document outlines the conventions to get your contribution accepted.
|
||||
We gratefully welcome improvements to documentation as well as code contributions.
|
||||
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and
|
||||
accepts contributions via GitHub pull requests. This document outlines
|
||||
some of the conventions on to make it easier to get your contribution
|
||||
accepted.
|
||||
|
||||
If you are new to the project, we recommend starting with documentation improvements or
|
||||
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
|
||||
```
|
||||
We gratefully welcome improvements to issues and documentation as well as to
|
||||
code.
|
||||
|
||||
## Certificate of Origin
|
||||
|
||||
By contributing to this project you agree to the Developer Certificate of Origin (DCO).
|
||||
This document was created by the Linux Kernel community and is a simple statement that you,
|
||||
as a contributor, have the legal right to make the contribution.
|
||||
By contributing to this project you agree to the Developer Certificate of
|
||||
Origin (DCO). This document was created by the Linux Kernel community and is a
|
||||
simple statement that you, as a contributor, have the legal right to make the
|
||||
contribution.
|
||||
|
||||
We require all commits to be signed. By signing off with your signature, you certify that you wrote
|
||||
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):
|
||||
We require all commits to be signed. By signing off with your signature, you
|
||||
certify that you wrote the patch or otherwise have the right to contribute the
|
||||
material by the rules of the [DCO](DCO):
|
||||
|
||||
`Signed-off-by: Jane Doe <jane.doe@example.com>`
|
||||
|
||||
The signature must contain your real name (sorry, no pseudonyms or anonymous contributions).
|
||||
If your `user.name` and `user.email` are set in your Git config,
|
||||
The signature must contain your real name
|
||||
(sorry, no pseudonyms or anonymous contributions)
|
||||
If your `user.name` and `user.email` are configured in your Git config,
|
||||
you can sign your commit automatically with `git commit -s`.
|
||||
|
||||
## Communications
|
||||
|
||||
For realtime communications we use Slack: To join the conversation, simply
|
||||
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the
|
||||
[#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
|
||||
|
||||
To discuss ideas and specifications we use [Github
|
||||
Discussions](https://github.com/fluxcd/flux2/discussions).
|
||||
|
||||
For announcements we use a mailing list as well. Simply subscribe to
|
||||
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev)
|
||||
to join the conversation (there you can also add calendar invites
|
||||
to your Google calendar for our [Flux
|
||||
meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)).
|
||||
|
||||
## Understanding Flux and the GitOps Toolkit
|
||||
|
||||
If you are entirely new to Flux and the GitOps Toolkit,
|
||||
you might want to take a look at the [introductory talk and demo](https://www.youtube.com/watch?v=qQBtSkgl7tI).
|
||||
|
||||
This project is composed of:
|
||||
|
||||
- [flux2](https://github.com/fluxcd/flux2): The Flux CLI
|
||||
- [source-manager](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git and Helm repositories, S3-compatible Buckets)
|
||||
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
||||
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
|
||||
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
|
||||
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
|
||||
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
|
||||
|
||||
### Understanding the code
|
||||
|
||||
To get started with developing controllers, you might want to review
|
||||
[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
|
||||
walks you through writing a short and concise controller that watches out
|
||||
for source changes.
|
||||
|
||||
## How to run the test suite
|
||||
|
||||
Prerequisites:
|
||||
|
||||
* go >= 1.24
|
||||
* kubectl >= 1.30
|
||||
* kustomize >= 5.0
|
||||
* coreutils (on Mac OS)
|
||||
|
||||
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
|
||||
|
||||
```bash
|
||||
make install-envtest
|
||||
```
|
||||
|
||||
Then you can run the unit tests with:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
|
||||
create a cluster for testing with:
|
||||
|
||||
```bash
|
||||
make setup-kind
|
||||
```
|
||||
|
||||
Then you can run the end-to-end tests with:
|
||||
|
||||
```bash
|
||||
make e2e
|
||||
```
|
||||
|
||||
When the output of the Flux CLI changes, to automatically update the golden
|
||||
files used in the test, pass `-update` flag to the test as:
|
||||
|
||||
```bash
|
||||
make e2e TEST_ARGS="-update"
|
||||
```
|
||||
|
||||
Since not all packages use golden files for testing, `-update` argument must be
|
||||
passed only for the packages that use golden files. Use the variables
|
||||
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
|
||||
path of the target test package:
|
||||
|
||||
```bash
|
||||
# Unit test
|
||||
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
||||
# e2e test
|
||||
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
||||
```
|
||||
|
||||
Teardown the e2e environment with:
|
||||
|
||||
```bash
|
||||
make cleanup-kind
|
||||
```
|
||||
|
||||
## Acceptance policy
|
||||
|
||||
These things will make a PR more likely to be accepted:
|
||||
|
||||
- Addressing an open issue, if one doesn't exist, please open an issue to discuss the problem and the proposed solution before submitting a PR.
|
||||
- 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.
|
||||
- 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.
|
||||
- All top-level Go code and exported names should have doc comments, as should non-trivial unexported type or function declarations.
|
||||
- Before submitting a PR, make sure that your code is properly formatted by running `make tidy fmt vet` and that all tests are passing by running `make test`.
|
||||
- a well-described requirement
|
||||
- tests for new code
|
||||
- tests for old code!
|
||||
- new code and tests follow the conventions in old code and tests
|
||||
- a good commit message (see below)
|
||||
- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1)
|
||||
- code must build on both Linux and Darwin, via plain `go build`
|
||||
- code should have appropriate test coverage and tests should be written
|
||||
to work with `go test`
|
||||
|
||||
In general, we will merge a PR once one maintainer has endorsed it.
|
||||
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.
|
||||
|
||||
## Format of the Commit Message
|
||||
### Format of the Commit Message
|
||||
|
||||
For the Flux project we prefer the following rules:
|
||||
For the GitOps Toolkit controllers we prefer the following rules for good commit messages:
|
||||
|
||||
- Limit the subject to 50 characters, start with a capital letter and do not end with a period.
|
||||
- Explain what and why in the body, if more than a trivial change; wrap it at 72 characters.
|
||||
- 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").
|
||||
- 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.
|
||||
- Limit the subject to 50 characters and write as the continuation
|
||||
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.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
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).
|
||||
The [following article](https://chris.beams.io/posts/git-commit/#seven-rules)
|
||||
has some more helpful advice on documenting your work.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
FROM alpine:3.23 AS builder
|
||||
FROM alpine:3.21 AS builder
|
||||
|
||||
RUN apk add --no-cache ca-certificates curl
|
||||
|
||||
ARG ARCH=linux/amd64
|
||||
ARG KUBECTL_VER=1.35.0
|
||||
ARG KUBECTL_VER=1.33.0
|
||||
|
||||
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
|
||||
|
||||
RUN kubectl version --client=true
|
||||
|
||||
FROM alpine:3.23 AS flux-cli
|
||||
FROM alpine:3.21 AS flux-cli
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
|
||||
all: test build
|
||||
|
||||
tidy:
|
||||
go mod tidy -compat=1.26
|
||||
cd tests/integration && go mod tidy -compat=1.26
|
||||
go mod tidy -compat=1.24
|
||||
cd tests/integration && go mod tidy -compat=1.24
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
@@ -52,14 +52,12 @@ guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/).
|
||||
|
||||
### Components
|
||||
|
||||
- [Source Controllers](https://fluxcd.io/flux/components/source/)
|
||||
- [Source Controller](https://fluxcd.io/flux/components/source/)
|
||||
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
|
||||
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
|
||||
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
|
||||
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
|
||||
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
|
||||
- [ExternalArtifact CRD](https://fluxcd.io/flux/components/source/externalartifacts/)
|
||||
- [ArtifactGenerator CRD](https://fluxcd.io/flux/components/source/artifactgenerators/)
|
||||
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
|
||||
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
|
||||
- [Helm Controller](https://fluxcd.io/flux/components/helm/)
|
||||
|
||||
@@ -16,24 +16,23 @@ inputs:
|
||||
description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
|
||||
required: false
|
||||
token:
|
||||
description: "Token used to authenticate against the GitHub.com API."
|
||||
description: "Token used to authentication against the GitHub.com API. Defaults to the token from the GitHub context of the workflow."
|
||||
required: false
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: "Download the binary to the runner's cache dir"
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: "${{ inputs.version }}"
|
||||
FLUX_TOOL_DIR: "${{ inputs.bindir }}"
|
||||
TOKEN: "${{ inputs.token }}"
|
||||
run: |
|
||||
VERSION=${{ inputs.version }}
|
||||
|
||||
TOKEN=${{ inputs.token }}
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
TOKEN=${{ github.token }}
|
||||
fi
|
||||
|
||||
if [[ -z "$VERSION" ]] || [[ "$VERSION" = "latest" ]]; then
|
||||
if [[ "${TOKEN}" != '' ]]; then
|
||||
VERSION=$(curl -fsSL -H "Authorization: token ${TOKEN}" https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f 4)
|
||||
else
|
||||
VERSION=$(curl -w "%{url_effective}\n" -IsSL https://github.com/fluxcd/flux2/releases/latest -o /dev/null | sed 's$^.*/$$')
|
||||
fi
|
||||
VERSION=$(curl -fsSL -H "Authorization: token ${TOKEN}" https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f 4)
|
||||
fi
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "Unable to determine Flux CLI version"
|
||||
@@ -60,6 +59,7 @@ runs:
|
||||
FLUX_EXEC_FILE="${FLUX_EXEC_FILE}.exe"
|
||||
fi
|
||||
|
||||
FLUX_TOOL_DIR=${{ inputs.bindir }}
|
||||
if [[ -z "$FLUX_TOOL_DIR" ]]; then
|
||||
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
|
||||
fi
|
||||
@@ -77,37 +77,9 @@ runs:
|
||||
|
||||
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
|
||||
|
||||
MAX_RETRIES=5
|
||||
RETRY_DELAY=5
|
||||
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
echo "Downloading flux binary (attempt $i/$MAX_RETRIES)"
|
||||
if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then
|
||||
break
|
||||
fi
|
||||
if [ $i -lt $MAX_RETRIES ]; then
|
||||
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
|
||||
sleep $RETRY_DELAY
|
||||
else
|
||||
echo "Failed to download flux binary after $MAX_RETRIES attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
echo "Downloading checksums file (attempt $i/$MAX_RETRIES)"
|
||||
if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then
|
||||
break
|
||||
fi
|
||||
if [ $i -lt $MAX_RETRIES ]; then
|
||||
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
|
||||
sleep $RETRY_DELAY
|
||||
else
|
||||
echo "Failed to download checksums file after $MAX_RETRIES attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"
|
||||
curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"
|
||||
|
||||
echo "Verifying checksum"
|
||||
sum=""
|
||||
if command -v openssl > /dev/null; then
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
)
|
||||
|
||||
// swapi.ArtifactGenerator
|
||||
|
||||
var artifactGeneratorType = apiType{
|
||||
kind: swapi.ArtifactGeneratorKind,
|
||||
humanKind: "artifactgenerator",
|
||||
groupVersion: swapi.GroupVersion,
|
||||
}
|
||||
|
||||
type artifactGeneratorAdapter struct {
|
||||
*swapi.ArtifactGenerator
|
||||
}
|
||||
|
||||
func (h artifactGeneratorAdapter) asClientObject() client.Object {
|
||||
return h.ArtifactGenerator
|
||||
}
|
||||
|
||||
func (h artifactGeneratorAdapter) deepCopyClientObject() client.Object {
|
||||
return h.ArtifactGenerator.DeepCopy()
|
||||
}
|
||||
|
||||
// swapi.ArtifactGeneratorList
|
||||
|
||||
type artifactGeneratorListAdapter struct {
|
||||
*swapi.ArtifactGeneratorList
|
||||
}
|
||||
|
||||
func (h artifactGeneratorListAdapter) asClientList() client.ObjectList {
|
||||
return h.ArtifactGeneratorList
|
||||
}
|
||||
|
||||
func (h artifactGeneratorListAdapter) len() int {
|
||||
return len(h.ArtifactGeneratorList.Items)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func init() {
|
||||
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components,
|
||||
"list of components, accepts comma-separated values")
|
||||
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil,
|
||||
"list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller,source-watcher'")
|
||||
"list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller'")
|
||||
|
||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd",
|
||||
"container registry where the Flux controller images are published")
|
||||
|
||||
@@ -42,7 +42,7 @@ import (
|
||||
var bootstrapGitLabCmd = &cobra.Command{
|
||||
Use: "gitlab",
|
||||
Short: "Deploy Flux on a cluster connected to a GitLab repository",
|
||||
Long: `The bootstrap gitlab command creates the GitLab repository if it doesn't exist and
|
||||
Long: `The bootstrap gitlab command creates the GitLab repository if it doesn't exists and
|
||||
commits the Flux manifests to the specified branch.
|
||||
Then it configures the target cluster to synchronize with that repository.
|
||||
If the Flux components are present on the cluster,
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -49,10 +48,9 @@ from the given directory or a single manifest file.`,
|
||||
}
|
||||
|
||||
type buildArtifactFlags struct {
|
||||
output string
|
||||
path string
|
||||
ignorePaths []string
|
||||
resolveSymlinks bool
|
||||
output string
|
||||
path string
|
||||
ignorePaths []string
|
||||
}
|
||||
|
||||
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
||||
@@ -63,7 +61,6 @@ func init() {
|
||||
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
|
||||
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.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().BoolVar(&buildArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
|
||||
|
||||
buildCmd.AddCommand(buildArtifactCmd)
|
||||
}
|
||||
@@ -88,15 +85,6 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
ociClient := oci.NewClient(oci.DefaultOptions())
|
||||
@@ -108,141 +96,6 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
||||
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) {
|
||||
b, err := io.ReadAll(bufio.NewReader(reader))
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,6 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -69,149 +68,3 @@ data:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func Test_resolveSymlinks(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create source directory with a real file
|
||||
srcDir := t.TempDir()
|
||||
realFile := filepath.Join(srcDir, "real.yaml")
|
||||
g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n name: test\n"), 0o644)).To(Succeed())
|
||||
|
||||
// Create a directory with symlinks pointing to files outside it
|
||||
symlinkDir := t.TempDir()
|
||||
symlinkFile := filepath.Join(symlinkDir, "linked.yaml")
|
||||
g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed())
|
||||
|
||||
// Also add a regular file in the symlink dir
|
||||
regularFile := filepath.Join(symlinkDir, "regular.yaml")
|
||||
g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed())
|
||||
|
||||
// Create a symlinked subdirectory
|
||||
subDir := filepath.Join(srcDir, "subdir")
|
||||
g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed())
|
||||
g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed())
|
||||
g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed())
|
||||
|
||||
// Resolve symlinks
|
||||
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
|
||||
g.Expect(err).To(BeNil())
|
||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||
|
||||
// Verify the regular file was copied
|
||||
content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n"))
|
||||
|
||||
// Verify the symlinked file was resolved and copied
|
||||
content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content)).To(ContainSubstring("kind: Namespace"))
|
||||
|
||||
// Verify that the resolved file is a regular file, not a symlink
|
||||
info, err := os.Lstat(filepath.Join(resolved, "linked.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
||||
|
||||
// Verify that the symlinked directory was resolved and its contents were copied
|
||||
content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content)).To(Equal("nested"))
|
||||
|
||||
// Verify that the file inside the symlinked directory is a regular file
|
||||
info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
||||
}
|
||||
|
||||
func Test_resolveSymlinks_singleFile(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create a real file
|
||||
srcDir := t.TempDir()
|
||||
realFile := filepath.Join(srcDir, "manifest.yaml")
|
||||
g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed())
|
||||
|
||||
// Create a symlink to the real file
|
||||
linkDir := t.TempDir()
|
||||
linkFile := filepath.Join(linkDir, "link.yaml")
|
||||
g.Expect(os.Symlink(realFile, linkFile)).To(Succeed())
|
||||
|
||||
// Resolve the single symlinked file
|
||||
resolved, cleanupDir, err := resolveSymlinks(linkFile)
|
||||
g.Expect(err).To(BeNil())
|
||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||
|
||||
// The returned path should be a file, not a directory
|
||||
info, err := os.Stat(resolved)
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(info.IsDir()).To(BeFalse())
|
||||
|
||||
// Verify contents
|
||||
content, err := os.ReadFile(resolved)
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content)).To(Equal("kind: ConfigMap"))
|
||||
}
|
||||
|
||||
func Test_resolveSymlinks_cycle(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create a directory with a symlink cycle: dir/link -> dir
|
||||
dir := t.TempDir()
|
||||
g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
||||
g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed())
|
||||
|
||||
// resolveSymlinks should not infinite-loop
|
||||
resolved, cleanupDir, err := resolveSymlinks(dir)
|
||||
g.Expect(err).To(BeNil())
|
||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||
|
||||
// The file should be copied
|
||||
content, err := os.ReadFile(filepath.Join(resolved, "file.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content)).To(Equal("data"))
|
||||
|
||||
// The cycle directory should exist but not cause infinite nesting
|
||||
_, err = os.Stat(filepath.Join(resolved, "cycle"))
|
||||
g.Expect(err).To(BeNil())
|
||||
|
||||
// There should NOT be deeply nested cycle/cycle/cycle/... paths
|
||||
_, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle"))
|
||||
g.Expect(os.IsNotExist(err)).To(BeTrue())
|
||||
}
|
||||
|
||||
func Test_resolveSymlinks_multipleLinksSameTarget(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create source directory with a real file inside a dir
|
||||
srcDir := t.TempDir()
|
||||
targetDir := filepath.Join(srcDir, "target")
|
||||
g.Expect(os.MkdirAll(targetDir, 0o755)).To(Succeed())
|
||||
g.Expect(os.WriteFile(filepath.Join(targetDir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
||||
|
||||
// Create a directory with multiple symlinks pointing to targetDir
|
||||
symlinkDir := t.TempDir()
|
||||
|
||||
// Link 1
|
||||
link1 := filepath.Join(symlinkDir, "link1")
|
||||
g.Expect(os.Symlink(targetDir, link1)).To(Succeed())
|
||||
|
||||
// Link 2
|
||||
link2 := filepath.Join(symlinkDir, "link2")
|
||||
g.Expect(os.Symlink(targetDir, link2)).To(Succeed())
|
||||
|
||||
// Resolve symlinks
|
||||
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
|
||||
g.Expect(err).To(BeNil())
|
||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||
|
||||
// Verify link1 has the file
|
||||
content, err := os.ReadFile(filepath.Join(resolved, "link1", "file.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content)).To(Equal("data"))
|
||||
|
||||
// Verify link2 ALSO has the file
|
||||
content2, err := os.ReadFile(filepath.Join(resolved, "link2", "file.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(string(content2)).To(Equal("data"))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -72,7 +71,6 @@ type buildKsFlags struct {
|
||||
strictSubst bool
|
||||
recursive bool
|
||||
localSources map[string]string
|
||||
inMemoryBuild bool
|
||||
}
|
||||
|
||||
var buildKsArgs buildKsFlags
|
||||
@@ -86,8 +84,6 @@ func init() {
|
||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||
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().BoolVar(&buildKsArgs.inMemoryBuild, "in-memory-build", true,
|
||||
"Use in-memory filesystem during build.")
|
||||
buildCmd.AddCommand(buildKsCmd)
|
||||
}
|
||||
|
||||
@@ -101,13 +97,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
||||
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
||||
}
|
||||
|
||||
// Normalize the path to handle Windows absolute and relative paths correctly
|
||||
buildKsArgs.path, err = filepath.Abs(buildKsArgs.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve absolute path: %w", err)
|
||||
}
|
||||
buildKsArgs.path = filepath.Clean(buildKsArgs.path)
|
||||
|
||||
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
|
||||
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
||||
}
|
||||
@@ -133,7 +122,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
||||
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
||||
build.WithRecursive(buildKsArgs.recursive),
|
||||
build.WithLocalSources(buildKsArgs.localSources),
|
||||
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
|
||||
)
|
||||
} else {
|
||||
builder, err = build.NewBuilder(name, buildKsArgs.path,
|
||||
@@ -144,7 +132,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
||||
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
||||
build.WithRecursive(buildKsArgs.recursive),
|
||||
build.WithLocalSources(buildKsArgs.localSources),
|
||||
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,12 +52,6 @@ func TestBuildKustomization(t *testing.T) {
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build podinfo (on-disk)",
|
||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build podinfo without service",
|
||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
||||
@@ -76,24 +70,12 @@ func TestBuildKustomization(t *testing.T) {
|
||||
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build ignore (on-disk)",
|
||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=false",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with recursive",
|
||||
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",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with recursive (on-disk)",
|
||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
}
|
||||
|
||||
tmpl := map[string]string{
|
||||
@@ -163,12 +145,6 @@ spec:
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build podinfo (on-disk)",
|
||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build podinfo without service",
|
||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
||||
@@ -199,18 +175,6 @@ spec:
|
||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with recursive (on-disk)",
|
||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with recursive in dry-run mode (on-disk)",
|
||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false --dry-run",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
}
|
||||
|
||||
tmpl := map[string]string{
|
||||
@@ -254,71 +218,3 @@ spec:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildKustomizationPathNormalization verifies that absolute and complex
|
||||
// paths are normalized to prevent path concatenation bugs (issue #5673).
|
||||
// Without normalization, paths could be duplicated like: /path/test/path/test/file
|
||||
func TestBuildKustomizationPathNormalization(t *testing.T) {
|
||||
// Get absolute path to testdata to test absolute path handling
|
||||
absTestDataPath, err := filepath.Abs("testdata/build-kustomization/podinfo")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get absolute path: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
resultFile string
|
||||
assertFunc string
|
||||
}{
|
||||
{
|
||||
name: "build with absolute path",
|
||||
args: "build kustomization podinfo --path " + absTestDataPath,
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with absolute path (on-disk)",
|
||||
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=false",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with complex relative path (parent dir)",
|
||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
{
|
||||
name: "build with path containing redundant separators",
|
||||
args: "build kustomization podinfo --path ./testdata//build-kustomization//podinfo",
|
||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||
assertFunc: "assertGoldenTemplateFile",
|
||||
},
|
||||
}
|
||||
|
||||
tmpl := map[string]string{
|
||||
"fluxns": allocateNamespace("flux-system"),
|
||||
}
|
||||
setup(t, tmpl)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var assert assertFunc
|
||||
|
||||
switch tt.assertFunc {
|
||||
case "assertGoldenTemplateFile":
|
||||
assert = assertGoldenTemplateFile(tt.resultFile, tmpl)
|
||||
case "assertError":
|
||||
assert = assertError(tt.resultFile)
|
||||
}
|
||||
|
||||
cmd := cmdTestCase{
|
||||
args: tt.args + " -n " + tmpl["fluxns"],
|
||||
assert: assert,
|
||||
}
|
||||
|
||||
cmd.runTestCmd(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ type checkFlags struct {
|
||||
}
|
||||
|
||||
var kubernetesConstraints = []string{
|
||||
">=1.33.0-0",
|
||||
">=1.31.0-0",
|
||||
}
|
||||
|
||||
var checkArgs checkFlags
|
||||
|
||||
@@ -94,13 +94,6 @@ var createHelmReleaseCmd = &cobra.Command{
|
||||
--source=HelmRepository/podinfo \
|
||||
--chart=podinfo
|
||||
|
||||
# Create a HelmRelease with custom storage namespace for hub-and-spoke model
|
||||
flux create hr podinfo \
|
||||
--target-namespace=production \
|
||||
--storage-namespace=fluxcd-system \
|
||||
--source=HelmRepository/podinfo \
|
||||
--chart=podinfo
|
||||
|
||||
# Create a HelmRelease using a source from a different namespace
|
||||
flux create hr podinfo \
|
||||
--namespace=default \
|
||||
@@ -134,7 +127,6 @@ type helmReleaseFlags struct {
|
||||
chartVersion string
|
||||
chartRef string
|
||||
targetNamespace string
|
||||
storageNamespace string
|
||||
createNamespace bool
|
||||
valuesFiles []string
|
||||
valuesFrom []string
|
||||
@@ -158,7 +150,6 @@ func init() {
|
||||
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)")
|
||||
createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.dependsOn, "depends-on", nil, "HelmReleases that must be ready before this release can be installed, supported formats '<name>' and '<namespace>/<name>'")
|
||||
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.targetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace")
|
||||
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.storageNamespace, "storage-namespace", "", "namespace to store the Helm release, defaults to the target namespace")
|
||||
createHelmReleaseCmd.Flags().BoolVar(&helmReleaseArgs.createNamespace, "create-target-namespace", false, "create the target namespace if it does not exist")
|
||||
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this HelmRelease")
|
||||
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart created by the helm release(accepted values: Revision and ChartRevision)")
|
||||
@@ -174,18 +165,10 @@ func init() {
|
||||
func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
if helmReleaseArgs.storageNamespace == "" && helmReleaseArgs.targetNamespace != "" {
|
||||
helmReleaseArgs.storageNamespace = helmReleaseArgs.targetNamespace
|
||||
}
|
||||
|
||||
if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" {
|
||||
return fmt.Errorf("chart or chart-ref is required")
|
||||
}
|
||||
|
||||
if helmReleaseArgs.chart != "" && helmReleaseArgs.chartRef != "" {
|
||||
return fmt.Errorf("cannot use --chart in combination with --chart-ref")
|
||||
}
|
||||
|
||||
sourceLabels, err := parseLabels()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -208,27 +191,15 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||
},
|
||||
Spec: helmv2.HelmReleaseSpec{
|
||||
ReleaseName: helmReleaseArgs.name,
|
||||
DependsOn: utils.MakeDependsOn(helmReleaseArgs.dependsOn),
|
||||
Interval: metav1.Duration{
|
||||
Duration: createArgs.interval,
|
||||
},
|
||||
TargetNamespace: helmReleaseArgs.targetNamespace,
|
||||
StorageNamespace: helmReleaseArgs.storageNamespace,
|
||||
Suspend: false,
|
||||
TargetNamespace: helmReleaseArgs.targetNamespace,
|
||||
Suspend: false,
|
||||
},
|
||||
}
|
||||
|
||||
if len(helmReleaseArgs.dependsOn) > 0 {
|
||||
ls := utils.MakeDependsOn(helmReleaseArgs.dependsOn)
|
||||
hrDependsOn := make([]helmv2.DependencyReference, 0, len(ls))
|
||||
for _, d := range ls {
|
||||
hrDependsOn = append(hrDependsOn, helmv2.DependencyReference{
|
||||
Name: d.Name,
|
||||
Namespace: d.Namespace,
|
||||
})
|
||||
}
|
||||
helmRelease.Spec.DependsOn = hrDependsOn
|
||||
}
|
||||
|
||||
switch {
|
||||
case helmReleaseArgs.chart != "":
|
||||
helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||
@@ -263,7 +234,7 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if helmReleaseArgs.kubeConfigSecretRef != "" {
|
||||
helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{
|
||||
SecretRef: &meta.SecretKeyReference{
|
||||
SecretRef: meta.SecretKeyReference{
|
||||
Name: helmReleaseArgs.kubeConfigSecretRef,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -42,11 +42,6 @@ func TestCreateHelmRelease(t *testing.T) {
|
||||
args: "create helmrelease podinfo --export",
|
||||
assert: assertError("chart or chart-ref is required"),
|
||||
},
|
||||
{
|
||||
name: "chart and chartRef used in combination",
|
||||
args: "create helmrelease podinfo --chart podinfo --chart-ref foobar/podinfo --export",
|
||||
assert: assertError("cannot use --chart in combination with --chart-ref"),
|
||||
},
|
||||
{
|
||||
name: "unknown source kind",
|
||||
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
|
||||
|
||||
@@ -29,18 +29,18 @@ import (
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var createImagePolicyCmd = &cobra.Command{
|
||||
Use: "policy [name]",
|
||||
Short: "Create or update an ImagePolicy object",
|
||||
Long: `The create image policy command generates an ImagePolicy resource.
|
||||
Long: withPreviewNote(`The create image policy command generates an ImagePolicy resource.
|
||||
An ImagePolicy object calculates a "latest image" given an image
|
||||
repository and a policy, e.g., semver.
|
||||
|
||||
The image that sorts highest according to the policy is recorded in
|
||||
the status of the object.`,
|
||||
the status of the object.`),
|
||||
Example: ` # Create an ImagePolicy to select the latest stable release
|
||||
flux create image policy podinfo \
|
||||
--image-ref=podinfo \
|
||||
@@ -81,6 +81,12 @@ func init() {
|
||||
createImageCmd.AddCommand(createImagePolicyCmd)
|
||||
}
|
||||
|
||||
// getObservedGeneration is implemented here, since it's not
|
||||
// (presently) needed elsewhere.
|
||||
func (obj imagePolicyAdapter) getObservedGeneration() int64 {
|
||||
return obj.ImagePolicy.Status.ObservedGeneration
|
||||
}
|
||||
|
||||
func createImagePolicyRun(cmd *cobra.Command, args []string) error {
|
||||
objectName := args[0]
|
||||
|
||||
|
||||
@@ -26,14 +26,14 @@ import (
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var createImageRepositoryCmd = &cobra.Command{
|
||||
Use: "repository [name]",
|
||||
Short: "Create or update an ImageRepository object",
|
||||
Long: `The create image repository command generates an ImageRepository resource.
|
||||
An ImageRepository object specifies an image repository to scan.`,
|
||||
Long: withPreviewNote(`The create image repository command generates an ImageRepository resource.
|
||||
An ImageRepository object specifies an image repository to scan.`),
|
||||
Example: ` # Create an ImageRepository object to scan the alpine image repository:
|
||||
flux create image repository alpine-repo --image alpine --interval 20m
|
||||
|
||||
|
||||
@@ -22,16 +22,16 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
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/v1beta2"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
)
|
||||
|
||||
var createImageUpdateCmd = &cobra.Command{
|
||||
Use: "update [name]",
|
||||
Short: "Create or update an ImageUpdateAutomation object",
|
||||
Long: `The create image update command generates an ImageUpdateAutomation resource.
|
||||
Long: withPreviewNote(`The create image update command generates an ImageUpdateAutomation resource.
|
||||
An ImageUpdateAutomation object specifies an automated update to images
|
||||
mentioned in YAMLs in a git repository.`,
|
||||
mentioned in YAMLs in a git repository.`),
|
||||
Example: ` # Configure image updates for the main repository created by flux bootstrap
|
||||
flux create image update flux-system \
|
||||
--git-repo-ref=flux-system \
|
||||
|
||||
@@ -136,9 +136,6 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
|
||||
return fmt.Errorf("path must begin with ./")
|
||||
}
|
||||
if kustomizationArgs.source.Name == "" {
|
||||
return fmt.Errorf("source is required")
|
||||
}
|
||||
|
||||
if !createArgs.export {
|
||||
logger.Generatef("generating Kustomization")
|
||||
@@ -156,6 +153,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
Labels: kslabels,
|
||||
},
|
||||
Spec: kustomizev1.KustomizationSpec{
|
||||
DependsOn: utils.MakeDependsOn(kustomizationArgs.dependsOn),
|
||||
Interval: metav1.Duration{
|
||||
Duration: createArgs.interval,
|
||||
},
|
||||
@@ -171,21 +169,9 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
},
|
||||
}
|
||||
|
||||
if len(kustomizationArgs.dependsOn) > 0 {
|
||||
ls := utils.MakeDependsOn(kustomizationArgs.dependsOn)
|
||||
ksDependsOn := make([]kustomizev1.DependencyReference, 0, len(ls))
|
||||
for _, d := range ls {
|
||||
ksDependsOn = append(ksDependsOn, kustomizev1.DependencyReference{
|
||||
Name: d.Name,
|
||||
Namespace: d.Namespace,
|
||||
})
|
||||
}
|
||||
kustomization.Spec.DependsOn = ksDependsOn
|
||||
}
|
||||
|
||||
if kustomizationArgs.kubeConfigSecretRef != "" {
|
||||
kustomization.Spec.KubeConfig = &meta.KubeConfigReference{
|
||||
SecretRef: &meta.SecretKeyReference{
|
||||
SecretRef: meta.SecretKeyReference{
|
||||
Name: kustomizationArgs.kubeConfigSecretRef,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//go:build unit
|
||||
// +build unit
|
||||
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCreateKustomization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
assert assertFunc
|
||||
}{
|
||||
{
|
||||
// A user creating a kustomization without --source gets a confusing
|
||||
// API-level error about spec.sourceRef.kind instead of a clear message.
|
||||
name: "missing source",
|
||||
args: "create kustomization my-app --path=./deploy --export",
|
||||
assert: assertError("source is required"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := cmdTestCase{
|
||||
args: tt.args,
|
||||
assert: tt.assert,
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
)
|
||||
|
||||
@@ -50,7 +49,7 @@ var createReceiverCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
type receiverFlags struct {
|
||||
receiverType flags.ReceiverType
|
||||
receiverType string
|
||||
secretRef string
|
||||
events []string
|
||||
resources []string
|
||||
@@ -59,7 +58,7 @@ type receiverFlags struct {
|
||||
var receiverArgs receiverFlags
|
||||
|
||||
func init() {
|
||||
createReceiverCmd.Flags().Var(&receiverArgs.receiverType, "type", receiverArgs.receiverType.Description())
|
||||
createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "")
|
||||
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
||||
@@ -110,7 +109,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
||||
Labels: sourceLabels,
|
||||
},
|
||||
Spec: notificationv1.ReceiverSpec{
|
||||
Type: receiverArgs.receiverType.String(),
|
||||
Type: receiverArgs.receiverType,
|
||||
Events: receiverArgs.events,
|
||||
Resources: resources,
|
||||
SecretRef: meta.LocalObjectReference{
|
||||
|
||||
@@ -56,22 +56,6 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
|
||||
}
|
||||
|
||||
existing.StringData = secret.StringData
|
||||
if secret.Annotations != nil {
|
||||
if existing.Annotations == nil {
|
||||
existing.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range secret.Annotations {
|
||||
existing.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
if secret.Labels != nil {
|
||||
if existing.Labels == nil {
|
||||
existing.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range secret.Labels {
|
||||
existing.Labels[k] = v
|
||||
}
|
||||
}
|
||||
if err := kubeClient.Update(ctx, &existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme)
|
||||
}
|
||||
|
||||
secret, err := sourcesecret.GenerateGit(opts)
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -46,18 +46,16 @@ var createSecretGitHubAppCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
type secretGitHubAppFlags struct {
|
||||
appID string
|
||||
appInstallationOwner string
|
||||
appInstallationID string
|
||||
privateKeyFile string
|
||||
baseURL string
|
||||
appID string
|
||||
appInstallationID string
|
||||
privateKeyFile string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
var secretGitHubAppArgs = secretGitHubAppFlags{}
|
||||
|
||||
func init() {
|
||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
|
||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationOwner, "app-installation-owner", "", "github app installation owner (user or organization)")
|
||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
|
||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
|
||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
|
||||
@@ -72,22 +70,36 @@ func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
|
||||
|
||||
secretName := args[0]
|
||||
|
||||
if secretGitHubAppArgs.appID == "" {
|
||||
return fmt.Errorf("--app-id is required")
|
||||
}
|
||||
|
||||
if secretGitHubAppArgs.appInstallationID == "" {
|
||||
return fmt.Errorf("--app-installation-id is required")
|
||||
}
|
||||
|
||||
if secretGitHubAppArgs.privateKeyFile == "" {
|
||||
return fmt.Errorf("--app-private-key is required")
|
||||
}
|
||||
|
||||
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read private key file: %w", err)
|
||||
}
|
||||
|
||||
opts := sourcesecret.Options{
|
||||
Name: secretName,
|
||||
Namespace: *kubeconfigArgs.Namespace,
|
||||
GitHubAppID: secretGitHubAppArgs.appID,
|
||||
GitHubAppInstallationOwner: secretGitHubAppArgs.appInstallationOwner,
|
||||
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
|
||||
GitHubAppPrivateKey: string(privateKey),
|
||||
GitHubAppBaseURL: secretGitHubAppArgs.baseURL,
|
||||
Name: secretName,
|
||||
Namespace: *kubeconfigArgs.Namespace,
|
||||
GitHubAppID: secretGitHubAppArgs.appID,
|
||||
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
|
||||
GitHubAppPrivateKey: string(privateKey),
|
||||
}
|
||||
|
||||
secret, err := sourcesecret.GenerateGitHubApp(opts)
|
||||
if secretGitHubAppArgs.baseURL != "" {
|
||||
opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL
|
||||
}
|
||||
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -31,6 +31,21 @@ func TestCreateSecretGitHubApp(t *testing.T) {
|
||||
args: "create secret githubapp",
|
||||
assert: assertError("name is required"),
|
||||
},
|
||||
{
|
||||
name: "create githubapp secret with missing app-id",
|
||||
args: "create secret githubapp appinfo",
|
||||
assert: assertError("--app-id is required"),
|
||||
},
|
||||
{
|
||||
name: "create githubapp secret with missing appInstallationID",
|
||||
args: "create secret githubapp appinfo --app-id 1",
|
||||
assert: assertError("--app-installation-id is required"),
|
||||
},
|
||||
{
|
||||
name: "create githubapp secret with missing private key file",
|
||||
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2",
|
||||
assert: assertError("--app-private-key is required"),
|
||||
},
|
||||
{
|
||||
name: "create githubapp secret with private key file that does not exist",
|
||||
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
|
||||
@@ -38,7 +53,7 @@ func TestCreateSecretGitHubApp(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "create githubapp secret with app info",
|
||||
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-owner my-org --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
|
||||
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
|
||||
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -83,12 +83,10 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
var certFile, keyFile []byte
|
||||
if secretHelmArgs.tlsCrtFile != "" {
|
||||
if secretHelmArgs.tlsCrtFile != "" && secretHelmArgs.tlsKeyFile != "" {
|
||||
if certFile, err = os.ReadFile(secretHelmArgs.tlsCrtFile); err != nil {
|
||||
return fmt.Errorf("failed to read cert file: %w", err)
|
||||
}
|
||||
}
|
||||
if secretHelmArgs.tlsKeyFile != "" {
|
||||
if keyFile, err = os.ReadFile(secretHelmArgs.tlsKeyFile); err != nil {
|
||||
return fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
@@ -104,7 +102,7 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
|
||||
TLSCrt: certFile,
|
||||
TLSKey: keyFile,
|
||||
}
|
||||
secret, err := sourcesecret.GenerateHelm(opts)
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func createSecretNotationCmdRun(cmd *cobra.Command, args []string) error {
|
||||
VerificationCrts: caCerts,
|
||||
TrustPolicy: policy,
|
||||
}
|
||||
secret, err := sourcesecret.GenerateNotation(opts)
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func createSecretOCICmdRun(cmd *cobra.Command, args []string) error {
|
||||
Username: secretOCIArgs.username,
|
||||
}
|
||||
|
||||
secret, err := sourcesecret.GenerateOCI(opts)
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func createSecretProxyCmdRun(cmd *cobra.Command, args []string) error {
|
||||
Username: secretProxyArgs.username,
|
||||
Password: secretProxyArgs.password,
|
||||
}
|
||||
secret, err := sourcesecret.GenerateProxy(opts)
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
||||
)
|
||||
|
||||
var createSecretReceiverCmd = &cobra.Command{
|
||||
Use: "receiver [name]",
|
||||
Short: "Create or update a Kubernetes secret for a Receiver webhook",
|
||||
Long: `The create secret receiver command generates a Kubernetes secret with
|
||||
the token used for webhook payload validation and an annotation with the
|
||||
computed webhook URL.`,
|
||||
Example: ` # Create a receiver secret for a GitHub webhook
|
||||
flux create secret receiver github-receiver \
|
||||
--namespace=my-namespace \
|
||||
--type=github \
|
||||
--hostname=flux.example.com \
|
||||
--export
|
||||
|
||||
# Create a receiver secret for GCR with email claim
|
||||
flux create secret receiver gcr-receiver \
|
||||
--namespace=my-namespace \
|
||||
--type=gcr \
|
||||
--hostname=flux.example.com \
|
||||
--email-claim=sa@project.iam.gserviceaccount.com \
|
||||
--export`,
|
||||
RunE: createSecretReceiverCmdRun,
|
||||
}
|
||||
|
||||
type secretReceiverFlags struct {
|
||||
receiverType flags.ReceiverType
|
||||
token string
|
||||
hostname string
|
||||
emailClaim string
|
||||
audienceClaim string
|
||||
}
|
||||
|
||||
var secretReceiverArgs secretReceiverFlags
|
||||
|
||||
func init() {
|
||||
createSecretReceiverCmd.Flags().Var(&secretReceiverArgs.receiverType, "type", secretReceiverArgs.receiverType.Description())
|
||||
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.token, "token", "", "webhook token used for payload validation and URL computation, auto-generated if not specified")
|
||||
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.hostname, "hostname", "", "hostname for the webhook URL e.g. flux.example.com")
|
||||
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.emailClaim, "email-claim", "", "IAM service account email, required for gcr type")
|
||||
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.audienceClaim, "audience-claim", "", "custom OIDC token audience for gcr type, defaults to the webhook URL")
|
||||
|
||||
createSecretCmd.AddCommand(createSecretReceiverCmd)
|
||||
}
|
||||
|
||||
func createSecretReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
if secretReceiverArgs.receiverType == "" {
|
||||
return fmt.Errorf("--type is required")
|
||||
}
|
||||
|
||||
if secretReceiverArgs.hostname == "" {
|
||||
return fmt.Errorf("--hostname is required")
|
||||
}
|
||||
|
||||
if secretReceiverArgs.receiverType.String() == notificationv1.GCRReceiver && secretReceiverArgs.emailClaim == "" {
|
||||
return fmt.Errorf("--email-claim is required for gcr receiver type")
|
||||
}
|
||||
|
||||
labels, err := parseLabels()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := sourcesecret.Options{
|
||||
Name: name,
|
||||
Namespace: *kubeconfigArgs.Namespace,
|
||||
Labels: labels,
|
||||
ReceiverType: secretReceiverArgs.receiverType.String(),
|
||||
Token: secretReceiverArgs.token,
|
||||
Hostname: secretReceiverArgs.hostname,
|
||||
EmailClaim: secretReceiverArgs.emailClaim,
|
||||
AudienceClaim: secretReceiverArgs.audienceClaim,
|
||||
}
|
||||
|
||||
secret, err := sourcesecret.GenerateReceiver(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createArgs.export {
|
||||
rootCmd.Println(secret.Content)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||
defer cancel()
|
||||
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var s corev1.Secret
|
||||
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertSecret(ctx, kubeClient, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Actionf("receiver secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
|
||||
return nil
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateReceiverSecret(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
assert assertFunc
|
||||
}{
|
||||
{
|
||||
name: "missing type",
|
||||
args: "create secret receiver test-secret --token=t --hostname=h",
|
||||
assert: assertError("--type is required"),
|
||||
},
|
||||
{
|
||||
name: "invalid type",
|
||||
args: "create secret receiver test-secret --type=invalid --token=t --hostname=h",
|
||||
assert: assertError("invalid argument \"invalid\" for \"--type\" flag: receiver type 'invalid' is not supported, must be one of: generic, generic-hmac, github, gitlab, bitbucket, harbor, dockerhub, quay, gcr, nexus, acr, cdevents"),
|
||||
},
|
||||
{
|
||||
name: "missing hostname",
|
||||
args: "create secret receiver test-secret --type=github --token=t",
|
||||
assert: assertError("--hostname is required"),
|
||||
},
|
||||
{
|
||||
name: "gcr missing email-claim",
|
||||
args: "create secret receiver test-secret --type=gcr --token=t --hostname=h",
|
||||
assert: assertError("--email-claim is required for gcr receiver type"),
|
||||
},
|
||||
{
|
||||
name: "github receiver secret",
|
||||
args: "create secret receiver receiver-secret --type=github --token=test-token --hostname=flux.example.com --namespace=my-namespace --export",
|
||||
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver.yaml"),
|
||||
},
|
||||
{
|
||||
name: "gcr receiver secret",
|
||||
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --namespace=my-namespace --export",
|
||||
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr.yaml"),
|
||||
},
|
||||
{
|
||||
name: "gcr receiver secret with custom audience",
|
||||
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --audience-claim=https://custom.audience.example.com --namespace=my-namespace --export",
|
||||
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := cmdTestCase{
|
||||
args: tt.args,
|
||||
assert: tt.assert,
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -84,18 +84,16 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if secretTLSArgs.tlsCrtFile != "" {
|
||||
if secretTLSArgs.tlsCrtFile != "" && secretTLSArgs.tlsKeyFile != "" {
|
||||
if opts.TLSCrt, err = os.ReadFile(secretTLSArgs.tlsCrtFile); err != nil {
|
||||
return fmt.Errorf("failed to read cert file: %w", err)
|
||||
}
|
||||
}
|
||||
if secretTLSArgs.tlsKeyFile != "" {
|
||||
if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil {
|
||||
return fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := sourcesecret.GenerateTLS(opts)
|
||||
secret, err := sourcesecret.Generate(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -113,6 +114,12 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
var ignorePaths *string
|
||||
if len(sourceBucketArgs.ignorePaths) > 0 {
|
||||
ignorePathsStr := strings.Join(sourceBucketArgs.ignorePaths, "\n")
|
||||
|
||||
@@ -44,26 +44,25 @@ import (
|
||||
)
|
||||
|
||||
type sourceGitFlags struct {
|
||||
url string
|
||||
branch string
|
||||
tag string
|
||||
semver string
|
||||
refName string
|
||||
commit string
|
||||
username string
|
||||
password string
|
||||
keyAlgorithm flags.PublicKeyAlgorithm
|
||||
keyRSABits flags.RSAKeyBits
|
||||
keyECDSACurve flags.ECDSACurve
|
||||
secretRef string
|
||||
proxySecretRef string
|
||||
provider flags.SourceGitProvider
|
||||
caFile string
|
||||
privateKeyFile string
|
||||
recurseSubmodules bool
|
||||
silent bool
|
||||
ignorePaths []string
|
||||
sparseCheckoutPaths []string
|
||||
url string
|
||||
branch string
|
||||
tag string
|
||||
semver string
|
||||
refName string
|
||||
commit string
|
||||
username string
|
||||
password string
|
||||
keyAlgorithm flags.PublicKeyAlgorithm
|
||||
keyRSABits flags.RSAKeyBits
|
||||
keyECDSACurve flags.ECDSACurve
|
||||
secretRef string
|
||||
proxySecretRef string
|
||||
provider flags.SourceGitProvider
|
||||
caFile string
|
||||
privateKeyFile string
|
||||
recurseSubmodules bool
|
||||
silent bool
|
||||
ignorePaths []string
|
||||
}
|
||||
|
||||
var createSourceGitCmd = &cobra.Command{
|
||||
@@ -155,7 +154,6 @@ func init() {
|
||||
"when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces")
|
||||
createSourceGitCmd.Flags().BoolVarP(&sourceGitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation")
|
||||
createSourceGitCmd.Flags().StringSliceVar(&sourceGitArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in git resource (can specify multiple paths with commas: path1,path2)")
|
||||
createSourceGitCmd.Flags().StringSliceVar(&sourceGitArgs.sparseCheckoutPaths, "sparse-checkout-paths", nil, "set paths to sparse checkout in git resource (can specify multiple paths with commas: path1,path2)")
|
||||
|
||||
createSourceCmd.AddCommand(createSourceGitCmd)
|
||||
}
|
||||
@@ -191,6 +189,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("specifying a CA file is not supported for Git over SSH")
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
sourceLabels, err := parseLabels()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -216,7 +220,6 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||
RecurseSubmodules: sourceGitArgs.recurseSubmodules,
|
||||
Reference: &sourcev1.GitRepositoryRef{},
|
||||
Ignore: ignorePaths,
|
||||
SparseCheckout: sourceGitArgs.sparseCheckoutPaths,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -299,7 +302,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||
secretOpts.Username = sourceGitArgs.username
|
||||
secretOpts.Password = sourceGitArgs.password
|
||||
}
|
||||
secret, err := sourcesecret.GenerateGit(secretOpts)
|
||||
secret, err := sourcesecret.Generate(secretOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (r *reconciler) conditionFunc() (bool, error) {
|
||||
}
|
||||
|
||||
func TestCreateSourceGitExport(t *testing.T) {
|
||||
var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --sparse-checkout-paths .cosign,non-existent-dir/ --ignore-paths .cosign,non-existent-dir/ -n default --interval 1m --export --timeout=" + testTimeout.String()
|
||||
var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --ignore-paths .cosign,non-existent-dir/ -n default --interval 1m --export --timeout=" + testTimeout.String()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -101,7 +101,7 @@ func TestCreateSourceGitExport(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "no args",
|
||||
args: "create source git --url=https://github.com/stefanprodan/podinfo",
|
||||
args: "create secret git",
|
||||
assert: assertError("name is required"),
|
||||
},
|
||||
{
|
||||
@@ -204,13 +204,12 @@ func TestCreateSourceGit(t *testing.T) {
|
||||
ObservedGeneration: repo.GetGeneration(),
|
||||
}
|
||||
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
|
||||
repo.Status.Artifact = &meta.Artifact{
|
||||
repo.Status.Artifact = &sourcev1.Artifact{
|
||||
Path: "some-path",
|
||||
Revision: "v1",
|
||||
LastUpdateTime: metav1.Time{
|
||||
Time: time.Now(),
|
||||
},
|
||||
Digest: "sha256:1234567890abcdef",
|
||||
}
|
||||
repo.Status.ObservedGeneration = repo.GetGeneration()
|
||||
},
|
||||
|
||||
@@ -114,6 +114,12 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if _, err := url.Parse(sourceHelmArgs.url); err != nil {
|
||||
return fmt.Errorf("url parse failed: %w", err)
|
||||
}
|
||||
@@ -196,7 +202,7 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
|
||||
TLSKey: keyFile,
|
||||
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
|
||||
}
|
||||
secret, err := sourcesecret.GenerateHelm(secretOpts)
|
||||
secret, err := sourcesecret.Generate(secretOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -31,8 +32,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
)
|
||||
|
||||
var createTenantCmd = &cobra.Command{
|
||||
@@ -58,10 +57,8 @@ const (
|
||||
)
|
||||
|
||||
type tenantFlags struct {
|
||||
namespaces []string
|
||||
clusterRole string
|
||||
account string
|
||||
skipNamespace bool
|
||||
namespaces []string
|
||||
clusterRole string
|
||||
}
|
||||
|
||||
var tenantArgs tenantFlags
|
||||
@@ -69,8 +66,6 @@ var tenantArgs tenantFlags
|
||||
func init() {
|
||||
createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant")
|
||||
createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding")
|
||||
createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant")
|
||||
createTenantCmd.Flags().BoolVar(&tenantArgs.skipNamespace, "skip-namespace", false, "skip namespace creation (namespace must exist already)")
|
||||
createCmd.AddCommand(createTenantCmd)
|
||||
}
|
||||
|
||||
@@ -112,17 +107,9 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
namespaces = append(namespaces, namespace)
|
||||
|
||||
accountName := tenant
|
||||
if tenantArgs.account != "" {
|
||||
accountName = tenantArgs.account
|
||||
}
|
||||
if err := validation.IsQualifiedName(accountName); len(err) > 0 {
|
||||
return fmt.Errorf("invalid service-account name '%s': %v", accountName, err)
|
||||
}
|
||||
|
||||
account := corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: accountName,
|
||||
Name: tenant,
|
||||
Namespace: ns,
|
||||
Labels: objLabels,
|
||||
},
|
||||
@@ -144,7 +131,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
||||
},
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: accountName,
|
||||
Name: tenant,
|
||||
Namespace: ns,
|
||||
},
|
||||
},
|
||||
@@ -159,7 +146,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if createArgs.export {
|
||||
for i := range tenantArgs.namespaces {
|
||||
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i], tenantArgs.skipNamespace); err != nil {
|
||||
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -175,11 +162,9 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
for i := range tenantArgs.namespaces {
|
||||
if !tenantArgs.skipNamespace {
|
||||
logger.Actionf("applying namespace %s", namespaces[i].Name)
|
||||
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Actionf("applying namespace %s", namespaces[i].Name)
|
||||
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Actionf("applying service account %s", accounts[i].Name)
|
||||
@@ -288,24 +273,19 @@ func upsertRoleBinding(ctx context.Context, kubeClient client.Client, roleBindin
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding, skipNamespace bool) error {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if !skipNamespace {
|
||||
namespace.TypeMeta = metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
}
|
||||
data, err = yaml.Marshal(namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
||||
|
||||
printlnStdout("---")
|
||||
printlnStdout(resourceToString(data))
|
||||
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding) error {
|
||||
namespace.TypeMeta = metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
}
|
||||
data, err := yaml.Marshal(namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("---")
|
||||
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
||||
fmt.Println(resourceToString(data))
|
||||
|
||||
account.TypeMeta = metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
@@ -315,10 +295,10 @@ func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, rol
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
||||
|
||||
printlnStdout("---")
|
||||
printlnStdout(resourceToString(data))
|
||||
fmt.Println("---")
|
||||
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
||||
fmt.Println(resourceToString(data))
|
||||
|
||||
roleBinding.TypeMeta = metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
@@ -329,8 +309,8 @@ func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, rol
|
||||
return err
|
||||
}
|
||||
|
||||
printlnStdout("---")
|
||||
printlnStdout(resourceToString(data))
|
||||
fmt.Println("---")
|
||||
fmt.Println(resourceToString(data))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
//go:build e2e
|
||||
// +build e2e
|
||||
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateTenant(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
assert assertFunc
|
||||
}{
|
||||
{
|
||||
name: "no args",
|
||||
args: "create tenant",
|
||||
assert: assertError("name is required"),
|
||||
},
|
||||
{
|
||||
name: "no namespace",
|
||||
args: "create tenant dev-team --cluster-role=cluster-admin",
|
||||
assert: assertError("with-namespace is required"),
|
||||
},
|
||||
{
|
||||
name: "basic tenant",
|
||||
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --export",
|
||||
assert: assertGoldenFile("./testdata/create_tenant/tenant-basic.yaml"),
|
||||
},
|
||||
{
|
||||
name: "tenant with custom serviceaccount",
|
||||
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --with-service-account=flux-tenant --export",
|
||||
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-service-account.yaml"),
|
||||
},
|
||||
{
|
||||
name: "tenant with custom cluster role",
|
||||
args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export",
|
||||
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"),
|
||||
},
|
||||
{
|
||||
name: "tenant with skip namespace",
|
||||
args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --skip-namespace --export",
|
||||
assert: assertGoldenFile("./testdata/create_tenant/tenant-with-skip-namespace.yaml"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := cmdTestCase{
|
||||
args: tt.args,
|
||||
assert: tt.assert,
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -40,19 +40,15 @@ WARNING: This command will print sensitive information if Kubernetes Secrets are
|
||||
flux debug hr podinfo --show-status
|
||||
|
||||
# Export the final values of a Helm release composed from referred ConfigMaps and Secrets
|
||||
flux debug hr podinfo --show-values > values.yaml
|
||||
|
||||
# Print the reconciliation history of a Helm release
|
||||
flux debug hr podinfo --show-history`,
|
||||
flux debug hr podinfo --show-values > values.yaml`,
|
||||
RunE: debugHelmReleaseCmdRun,
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
||||
}
|
||||
|
||||
type debugHelmReleaseFlags struct {
|
||||
showStatus bool
|
||||
showValues bool
|
||||
showHistory bool
|
||||
showStatus bool
|
||||
showValues bool
|
||||
}
|
||||
|
||||
var debugHelmReleaseArgs debugHelmReleaseFlags
|
||||
@@ -60,25 +56,15 @@ var debugHelmReleaseArgs debugHelmReleaseFlags
|
||||
func init() {
|
||||
debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showStatus, "show-status", false, "print the status of the Helm release")
|
||||
debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showValues, "show-values", false, "print the final values of the Helm release")
|
||||
debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showHistory, "show-history", false, "print the reconciliation history of the Helm release")
|
||||
debugCmd.AddCommand(debugHelmReleaseCmd)
|
||||
}
|
||||
|
||||
func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
flagsSet := 0
|
||||
if debugHelmReleaseArgs.showStatus {
|
||||
flagsSet++
|
||||
}
|
||||
if debugHelmReleaseArgs.showValues {
|
||||
flagsSet++
|
||||
}
|
||||
if debugHelmReleaseArgs.showHistory {
|
||||
flagsSet++
|
||||
}
|
||||
if flagsSet != 1 {
|
||||
return fmt.Errorf("exactly one of --show-status, --show-values, or --show-history must be set")
|
||||
if (!debugHelmReleaseArgs.showStatus && !debugHelmReleaseArgs.showValues) ||
|
||||
(debugHelmReleaseArgs.showStatus && debugHelmReleaseArgs.showValues) {
|
||||
return fmt.Errorf("either --show-status or --show-values must be set")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||
@@ -123,20 +109,5 @@ func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||
rootCmd.Print(string(values))
|
||||
}
|
||||
|
||||
if debugHelmReleaseArgs.showHistory {
|
||||
if len(hr.Status.History) == 0 {
|
||||
hr.Status.History = helmv2.Snapshots{}
|
||||
}
|
||||
|
||||
history, err := yaml.Marshal(hr.Status.History)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootCmd.Println("# History documentation: https://fluxcd.io/flux/components/helm/helmreleases/#history")
|
||||
rootCmd.Print(string(history))
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,18 +56,6 @@ func TestDebugHelmRelease(t *testing.T) {
|
||||
"testdata/debug_helmrelease/values-from.golden.yaml",
|
||||
tmpl,
|
||||
},
|
||||
{
|
||||
"debug history",
|
||||
"debug helmrelease test-with-history --show-history --show-status=false",
|
||||
"testdata/debug_helmrelease/history.golden.yaml",
|
||||
tmpl,
|
||||
},
|
||||
{
|
||||
"debug history empty",
|
||||
"debug helmrelease test-values-inline --show-history --show-status=false",
|
||||
"testdata/debug_helmrelease/history-empty.golden.yaml",
|
||||
tmpl,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"strings"
|
||||
|
||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/kustomize"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -45,19 +44,15 @@ WARNING: This command will print sensitive information if Kubernetes Secrets are
|
||||
flux debug ks podinfo --show-status
|
||||
|
||||
# Export the final variables used for post-build substitutions composed from referred ConfigMaps and Secrets
|
||||
flux debug ks podinfo --show-vars > vars.env
|
||||
|
||||
# Print the reconciliation history of a Flux Kustomization
|
||||
flux debug ks podinfo --show-history`,
|
||||
flux debug ks podinfo --show-vars > vars.env`,
|
||||
RunE: debugKustomizationCmdRun,
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
||||
}
|
||||
|
||||
type debugKustomizationFlags struct {
|
||||
showStatus bool
|
||||
showVars bool
|
||||
showHistory bool
|
||||
showStatus bool
|
||||
showVars bool
|
||||
}
|
||||
|
||||
var debugKustomizationArgs debugKustomizationFlags
|
||||
@@ -65,25 +60,15 @@ var debugKustomizationArgs debugKustomizationFlags
|
||||
func init() {
|
||||
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showStatus, "show-status", false, "print the status of the Flux Kustomization")
|
||||
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showVars, "show-vars", false, "print the final vars of the Flux Kustomization in dot env format")
|
||||
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showHistory, "show-history", false, "print the reconciliation history of the Flux Kustomization")
|
||||
debugCmd.AddCommand(debugKustomizationCmd)
|
||||
}
|
||||
|
||||
func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
flagsSet := 0
|
||||
if debugKustomizationArgs.showStatus {
|
||||
flagsSet++
|
||||
}
|
||||
if debugKustomizationArgs.showVars {
|
||||
flagsSet++
|
||||
}
|
||||
if debugKustomizationArgs.showHistory {
|
||||
flagsSet++
|
||||
}
|
||||
if flagsSet != 1 {
|
||||
return fmt.Errorf("exactly one of --show-status, --show-vars, or --show-history must be set")
|
||||
if (!debugKustomizationArgs.showStatus && !debugKustomizationArgs.showVars) ||
|
||||
(debugKustomizationArgs.showStatus && debugKustomizationArgs.showVars) {
|
||||
return fmt.Errorf("either --show-status or --show-vars must be set")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||
@@ -145,20 +130,5 @@ func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if debugKustomizationArgs.showHistory {
|
||||
if len(ks.Status.History) == 0 {
|
||||
ks.Status.History = meta.History{}
|
||||
}
|
||||
|
||||
history, err := yaml.Marshal(ks.Status.History)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootCmd.Println("# History documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#history")
|
||||
rootCmd.Print(string(history))
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -55,17 +55,6 @@ func TestDebugKustomization(t *testing.T) {
|
||||
"debug ks test-from --show-vars --show-status=false",
|
||||
"testdata/debug_kustomization/vars-from.golden.env",
|
||||
tmpl,
|
||||
}, {
|
||||
"debug history",
|
||||
"debug ks test-with-history --show-history --show-status=false",
|
||||
"testdata/debug_kustomization/history.golden.yaml",
|
||||
tmpl,
|
||||
},
|
||||
{
|
||||
"debug history empty",
|
||||
"debug ks test --show-history --show-status=false",
|
||||
"testdata/debug_kustomization/history-empty.golden.yaml",
|
||||
tmpl,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ package main
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var deleteImagePolicyCmd = &cobra.Command{
|
||||
Use: "policy [name]",
|
||||
Short: "Delete an ImagePolicy object",
|
||||
Long: `The delete image policy command deletes the given ImagePolicy from the cluster.`,
|
||||
Long: withPreviewNote(`The delete image policy command deletes the given ImagePolicy from the cluster.`),
|
||||
Example: ` # Delete an image policy
|
||||
flux delete image policy alpine3.x`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)),
|
||||
|
||||
@@ -19,13 +19,13 @@ package main
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var deleteImageRepositoryCmd = &cobra.Command{
|
||||
Use: "repository [name]",
|
||||
Short: "Delete an ImageRepository object",
|
||||
Long: "The delete image repository command deletes the given ImageRepository from the cluster.",
|
||||
Long: withPreviewNote("The delete image repository command deletes the given ImageRepository from the cluster."),
|
||||
Example: ` # Delete an image repository
|
||||
flux delete image repository alpine`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)),
|
||||
|
||||
@@ -19,13 +19,13 @@ package main
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var deleteImageUpdateCmd = &cobra.Command{
|
||||
Use: "update [name]",
|
||||
Short: "Delete an ImageUpdateAutomation object",
|
||||
Long: `The delete image update command deletes the given ImageUpdateAutomation from the cluster.`,
|
||||
Long: withPreviewNote(`The delete image update command deletes the given ImageUpdateAutomation from the cluster.`),
|
||||
Example: ` # Delete an image update automation
|
||||
flux delete image update latest-images`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)),
|
||||
|
||||
@@ -93,7 +93,7 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if diffArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||
logger.Actionf("logging in to registry with provider credentials")
|
||||
opt, _, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String())
|
||||
opt, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during login with provider: %w", err)
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ type diffKsFlags struct {
|
||||
strictSubst bool
|
||||
recursive bool
|
||||
localSources map[string]string
|
||||
inMemoryBuild bool
|
||||
ignoreNotFound bool
|
||||
}
|
||||
|
||||
var diffKsArgs diffKsFlags
|
||||
@@ -77,10 +75,6 @@ func init() {
|
||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||
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().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true,
|
||||
"Use in-memory filesystem during build.")
|
||||
diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false,
|
||||
"Ignore Kustomization not found errors on the cluster when diffing.")
|
||||
diffCmd.AddCommand(diffKsCmd)
|
||||
}
|
||||
|
||||
@@ -119,8 +113,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
build.WithRecursive(diffKsArgs.recursive),
|
||||
build.WithLocalSources(diffKsArgs.localSources),
|
||||
build.WithSingleKustomization(),
|
||||
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
|
||||
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
|
||||
)
|
||||
} else {
|
||||
builder, err = build.NewBuilder(name, diffKsArgs.path,
|
||||
@@ -132,8 +124,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
build.WithRecursive(diffKsArgs.recursive),
|
||||
build.WithLocalSources(diffKsArgs.localSources),
|
||||
build.WithSingleKustomization(),
|
||||
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
|
||||
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/build"
|
||||
"github.com/fluxcd/pkg/ssa"
|
||||
"github.com/fluxcd/pkg/ssa/normalize"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
@@ -48,7 +47,7 @@ func TestDiffKustomization(t *testing.T) {
|
||||
name: "diff nothing deployed",
|
||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
||||
objectFile: "",
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
||||
},
|
||||
{
|
||||
name: "diff with a deployment object",
|
||||
@@ -96,7 +95,7 @@ func TestDiffKustomization(t *testing.T) {
|
||||
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",
|
||||
objectFile: "",
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
||||
},
|
||||
{
|
||||
name: "diff with recursive",
|
||||
@@ -138,118 +137,6 @@ func TestDiffKustomization(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiffKustomizationNotDeployed tests `flux diff ks` when the Kustomization
|
||||
// CR does not exist in the cluster but is provided via --kustomization-file.
|
||||
// Reproduces https://github.com/fluxcd/flux2/issues/5439
|
||||
func TestDiffKustomizationNotDeployed(t *testing.T) {
|
||||
// Use a dedicated namespace with NO setup() -- the Kustomization CR
|
||||
// intentionally does not exist in the cluster.
|
||||
tmpl := map[string]string{
|
||||
"fluxns": allocateNamespace("flux-system"),
|
||||
}
|
||||
setupTestNamespace(tmpl["fluxns"], t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
assert assertFunc
|
||||
}{
|
||||
{
|
||||
name: "fails without --ignore-not-found",
|
||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml",
|
||||
assert: assertError("failed to get kustomization object: kustomizations.kustomize.toolkit.fluxcd.io \"podinfo\" not found"),
|
||||
},
|
||||
{
|
||||
name: "succeeds with --ignore-not-found and --kustomization-file",
|
||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml " +
|
||||
"--ignore-not-found",
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := cmdTestCase{
|
||||
args: tt.args + " -n " + tmpl["fluxns"],
|
||||
assert: tt.assert,
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiffKustomizationTakeOwnership tests `flux diff ks` when taking ownership
|
||||
// of existing resources on the cluster. A "pre-existing" configmap is applied
|
||||
// to the cluster, and the kustomization contains a matching configmap; the
|
||||
// diff should show the labels added by flux
|
||||
func TestDiffKustomizationTakeOwnership(t *testing.T) {
|
||||
tmpl := map[string]string{
|
||||
"fluxns": allocateNamespace("flux-system"),
|
||||
}
|
||||
setupTestNamespace(tmpl["fluxns"], t)
|
||||
|
||||
b, _ := build.NewBuilder("configmaps", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
|
||||
resourceManager, err := b.Manager()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Pre-create the "existing" configmap in the cluster without Flux labels
|
||||
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/existing-configmap.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := cmdTestCase{
|
||||
args: "diff kustomization configmaps --path ./testdata/build-kustomization/configmaps --progress-bar=false " +
|
||||
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-configmaps.yaml " +
|
||||
"--ignore-not-found" +
|
||||
" -n " + tmpl["fluxns"],
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-taking-ownership.golden"),
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
}
|
||||
|
||||
// TestDiffKustomizationNewNamespaceAndConfigmap runs `flux diff ks` when the
|
||||
// kustomization creates a new namespace and resources inside it. The server-side
|
||||
// dry-run cannot resolve resources in a namespace that doesn't exist yet,
|
||||
// consistent with `kubectl diff --server-side` behavior.
|
||||
func TestDiffKustomizationNewNamespaceAndConfigmap(t *testing.T) {
|
||||
tmpl := map[string]string{
|
||||
"fluxns": allocateNamespace("flux-system"),
|
||||
}
|
||||
setupTestNamespace(tmpl["fluxns"], t)
|
||||
|
||||
cmd := cmdTestCase{
|
||||
args: "diff kustomization new-namespace-and-configmap --path ./testdata/build-kustomization/new-namespace-and-configmap --progress-bar=false " +
|
||||
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml " +
|
||||
"--ignore-not-found" +
|
||||
" -n " + tmpl["fluxns"],
|
||||
assert: assertError("ConfigMap/new-ns/app-config not found: namespaces \"new-ns\" not found"),
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
}
|
||||
|
||||
// TestDiffKustomizationNewNamespaceOnly runs `flux diff ks` when the
|
||||
// kustomization creates only a new namespace. The diff should show the
|
||||
// namespace as created.
|
||||
func TestDiffKustomizationNewNamespaceOnly(t *testing.T) {
|
||||
tmpl := map[string]string{
|
||||
"fluxns": allocateNamespace("flux-system"),
|
||||
}
|
||||
setupTestNamespace(tmpl["fluxns"], t)
|
||||
|
||||
cmd := cmdTestCase{
|
||||
args: "diff kustomization new-namespace-only --path ./testdata/build-kustomization/new-namespace-only --progress-bar=false " +
|
||||
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml " +
|
||||
"--ignore-not-found" +
|
||||
" -n " + tmpl["fluxns"],
|
||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-namespace-only.golden"),
|
||||
}
|
||||
cmd.runTestCmd(t)
|
||||
}
|
||||
|
||||
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
||||
buf, err := os.ReadFile(objectFile)
|
||||
if err != nil {
|
||||
@@ -264,7 +151,7 @@ func createObjectFromFile(objectFile string, templateValues map[string]string, t
|
||||
t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err)
|
||||
}
|
||||
|
||||
if err := normalize.UnstructuredList(clientObjects); err != nil {
|
||||
if err := ssa.SetNativeKindsDefaults(clientObjects); err != nil {
|
||||
t.Fatalf("Error setting native kinds defaults for '%s': %v", objectFile, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -39,13 +40,12 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||
@@ -112,12 +112,7 @@ func eventsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
var diffRefNs bool
|
||||
// Build the base list options. When --all-namespaces is set we must NOT constrain the
|
||||
// query to a single namespace, otherwise we silently return a partial result set.
|
||||
clientListOpts := []client.ListOption{}
|
||||
if !eventArgs.allNamespaces {
|
||||
clientListOpts = append(clientListOpts, client.InNamespace(*kubeconfigArgs.Namespace))
|
||||
}
|
||||
clientListOpts := []client.ListOption{client.InNamespace(*kubeconfigArgs.Namespace)}
|
||||
var refListOpts [][]client.ListOption
|
||||
if eventArgs.forSelector != "" {
|
||||
kind, name := getKindNameFromSelector(eventArgs.forSelector)
|
||||
@@ -196,14 +191,11 @@ func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []cli
|
||||
|
||||
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
||||
listOpts := &metav1.ListOptions{}
|
||||
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||
err := runtimeresource.FollowContinue(listOpts,
|
||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
newEvents := &corev1.EventList{}
|
||||
opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||
if options.Continue != "" {
|
||||
opts = append(opts, client.Continue(options.Continue))
|
||||
}
|
||||
if err := kubeclient.List(ctx, newEvents, opts...); err != nil {
|
||||
if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
|
||||
return nil, fmt.Errorf("error getting events: %w", err)
|
||||
}
|
||||
el.Items = append(el.Items, newEvents.Items...)
|
||||
@@ -254,7 +246,7 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt
|
||||
hdr = getHeaders(showNs)
|
||||
firstIteration = false
|
||||
}
|
||||
return printers.TablePrinter(hdr).Print(rootCmd.OutOrStdout(), [][]string{rows})
|
||||
return printers.TablePrinter(hdr).Print(os.Stdout, [][]string{rows})
|
||||
}
|
||||
|
||||
for _, refOpts := range refListOpts {
|
||||
@@ -458,7 +450,6 @@ var fluxKindMap = refMap{
|
||||
sourcev1.HelmRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)},
|
||||
autov1.ImageUpdateAutomationKind: {gvk: autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)},
|
||||
imagev1.ImageRepositoryKind: {gvk: imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)},
|
||||
swapi.ArtifactGeneratorKind: {gvk: swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)},
|
||||
}
|
||||
|
||||
func ignoreEvent(e corev1.Event) bool {
|
||||
|
||||
@@ -20,13 +20,11 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -142,7 +140,7 @@ spec:
|
||||
address: https://hooks.slack.com/services/mock
|
||||
type: slack
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: podinfo
|
||||
@@ -421,108 +419,6 @@ func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event
|
||||
}
|
||||
}
|
||||
|
||||
// paginatedClient wraps a client.Client and simulates real Kubernetes API
|
||||
// pagination by splitting List results into pages of pageSize items,
|
||||
// using the ListMeta.Continue token.
|
||||
type paginatedClient struct {
|
||||
client.Client
|
||||
pageSize int
|
||||
}
|
||||
|
||||
func (c *paginatedClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||
listOpts := &client.ListOptions{}
|
||||
listOpts.ApplyOptions(opts)
|
||||
|
||||
// Fetch all results from the underlying client (without Limit/Continue).
|
||||
stripped := make([]client.ListOption, 0, len(opts))
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(client.Limit); ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := o.(client.Continue); ok {
|
||||
continue
|
||||
}
|
||||
stripped = append(stripped, o)
|
||||
}
|
||||
if err := c.Client.List(ctx, list, stripped...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items, err := meta.ExtractList(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine the page window based on the Continue token.
|
||||
start := 0
|
||||
if listOpts.Continue != "" {
|
||||
n, err := strconv.Atoi(listOpts.Continue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid continue token: %w", err)
|
||||
}
|
||||
start = n
|
||||
}
|
||||
if start > len(items) {
|
||||
start = len(items)
|
||||
}
|
||||
|
||||
end := start + c.pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
|
||||
page := items[start:end]
|
||||
if err := meta.SetList(list, page); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the Continue token when there are more pages.
|
||||
listAccessor, err := meta.ListAccessor(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if end < len(items) {
|
||||
listAccessor.SetContinue(strconv.Itoa(end))
|
||||
} else {
|
||||
listAccessor.SetContinue("")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_addEventsToList_pagination(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
objs, err := ssautil.ReadObjects(strings.NewReader(objects))
|
||||
g.Expect(err).To(Not(HaveOccurred()))
|
||||
|
||||
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
|
||||
for _, obj := range objs {
|
||||
builder = builder.WithObjects(obj)
|
||||
}
|
||||
|
||||
eventList := &corev1.EventList{}
|
||||
for _, obj := range objs {
|
||||
infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason")
|
||||
warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason")
|
||||
eventList.Items = append(eventList.Items, infoEvent, warningEvent)
|
||||
}
|
||||
builder = builder.WithLists(eventList)
|
||||
c := builder.Build()
|
||||
|
||||
totalEvents := len(eventList.Items)
|
||||
g.Expect(totalEvents).To(BeNumerically(">", 2), "need more than 2 events to test pagination")
|
||||
|
||||
// Wrap the client to paginate at 2 items per page, forcing multiple
|
||||
// round-trips through FollowContinue.
|
||||
pc := &paginatedClient{Client: c, pageSize: 2}
|
||||
|
||||
el := &corev1.EventList{}
|
||||
err = addEventsToList(context.Background(), pc, el, nil)
|
||||
g.Expect(err).To(Not(HaveOccurred()))
|
||||
g.Expect(el.Items).To(HaveLen(totalEvents),
|
||||
"addEventsToList should collect all events across paginated responses")
|
||||
}
|
||||
|
||||
func kindNameIndexer(obj client.Object) []string {
|
||||
e, ok := obj.(*corev1.Event)
|
||||
if !ok {
|
||||
|
||||
@@ -109,13 +109,13 @@ func (export exportCommand) run(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func printExport(export any) error {
|
||||
func printExport(export interface{}) error {
|
||||
data, err := yaml.Marshal(export)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printlnStdout("---")
|
||||
printlnStdout(resourceToString(data))
|
||||
rootCmd.Println("---")
|
||||
rootCmd.Println(resourceToString(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var exportArtifactCmd = &cobra.Command{
|
||||
Use: "artifact",
|
||||
Short: "Export artifact objects",
|
||||
Long: `The export artifact sub-commands export artifacts objects in YAML format.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
exportCmd.AddCommand(exportArtifactCmd)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
)
|
||||
|
||||
var exportArtifactGeneratorCmd = &cobra.Command{
|
||||
Use: "generator [name]",
|
||||
Short: "Export ArtifactGenerator resources in YAML format",
|
||||
Long: "The export artifact generator command exports one or all ArtifactGenerator resources in YAML format.",
|
||||
Example: ` # Export all ArtifactGenerator resources
|
||||
flux export artifact generator --all > artifact-generators.yaml
|
||||
|
||||
# Export a specific generator
|
||||
flux export artifact generator my-generator > my-generator.yaml`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
|
||||
RunE: exportCommand{
|
||||
object: artifactGeneratorAdapter{&swapi.ArtifactGenerator{}},
|
||||
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
|
||||
}.run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
exportArtifactCmd.AddCommand(exportArtifactGeneratorCmd)
|
||||
}
|
||||
|
||||
// Export returns an ArtifactGenerator value which has
|
||||
// extraneous information stripped out.
|
||||
func exportArtifactGenerator(item *swapi.ArtifactGenerator) interface{} {
|
||||
gvk := swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)
|
||||
export := swapi.ArtifactGenerator{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: gvk.Kind,
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: item.Name,
|
||||
Namespace: item.Namespace,
|
||||
Labels: item.Labels,
|
||||
Annotations: item.Annotations,
|
||||
},
|
||||
Spec: item.Spec,
|
||||
}
|
||||
return export
|
||||
}
|
||||
|
||||
func (ex artifactGeneratorAdapter) export() interface{} {
|
||||
return exportArtifactGenerator(ex.ArtifactGenerator)
|
||||
}
|
||||
|
||||
func (ex artifactGeneratorListAdapter) exportItem(i int) interface{} {
|
||||
return exportArtifactGenerator(&ex.ArtifactGeneratorList.Items[i])
|
||||
}
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var exportImagePolicyCmd = &cobra.Command{
|
||||
Use: "policy [name]",
|
||||
Short: "Export ImagePolicy resources in YAML format",
|
||||
Long: "The export image policy command exports one or all ImagePolicy resources in YAML format.",
|
||||
Long: withPreviewNote("The export image policy command exports one or all ImagePolicy resources in YAML format."),
|
||||
Example: ` # Export all ImagePolicy resources
|
||||
flux export image policy --all > image-policies.yaml
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var exportImageRepositoryCmd = &cobra.Command{
|
||||
Use: "repository [name]",
|
||||
Short: "Export ImageRepository resources in YAML format",
|
||||
Long: "The export image repository command exports one or all ImageRepository resources in YAML format.",
|
||||
Long: withPreviewNote("The export image repository command exports one or all ImageRepository resources in YAML format."),
|
||||
Example: ` # Export all ImageRepository resources
|
||||
flux export image repository --all > image-repositories.yaml
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
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/v1beta2"
|
||||
)
|
||||
|
||||
var exportImageUpdateCmd = &cobra.Command{
|
||||
Use: "update [name]",
|
||||
Short: "Export ImageUpdateAutomation resources in YAML format",
|
||||
Long: "The export image update command exports one or all ImageUpdateAutomation resources in YAML format.",
|
||||
Long: withPreviewNote("The export image update command exports one or all ImageUpdateAutomation resources in YAML format."),
|
||||
Example: ` # Export all ImageUpdateAutomation resources
|
||||
flux export image update --all > updates.yaml
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
)
|
||||
|
||||
var exportSourceExternalCmd = &cobra.Command{
|
||||
Use: "external [name]",
|
||||
Short: "Export ExternalArtifact sources in YAML format",
|
||||
Long: "The export source external command exports one or all ExternalArtifact sources in YAML format.",
|
||||
Example: ` # Export all ExternalArtifact sources
|
||||
flux export source external --all > sources.yaml
|
||||
|
||||
# Export a specific ExternalArtifact
|
||||
flux export source external my-artifact > source.yaml`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
|
||||
RunE: exportWithSecretCommand{
|
||||
list: externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||
object: externalArtifactAdapter{&sourcev1.ExternalArtifact{}},
|
||||
}.run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
exportSourceCmd.AddCommand(exportSourceExternalCmd)
|
||||
}
|
||||
|
||||
func exportExternalArtifact(source *sourcev1.ExternalArtifact) any {
|
||||
gvk := sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)
|
||||
export := sourcev1.ExternalArtifact{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: gvk.Kind,
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: source.Name,
|
||||
Namespace: source.Namespace,
|
||||
Labels: source.Labels,
|
||||
Annotations: source.Annotations,
|
||||
},
|
||||
Spec: source.Spec,
|
||||
}
|
||||
return export
|
||||
}
|
||||
|
||||
func getExternalArtifactSecret(source *sourcev1.ExternalArtifact) *types.NamespacedName {
|
||||
// ExternalArtifact does not have a secretRef in its spec, this satisfies the interface
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ex externalArtifactAdapter) secret() *types.NamespacedName {
|
||||
return getExternalArtifactSecret(ex.ExternalArtifact)
|
||||
}
|
||||
|
||||
func (ex externalArtifactListAdapter) secretItem(i int) *types.NamespacedName {
|
||||
return getExternalArtifactSecret(&ex.ExternalArtifactList.Items[i])
|
||||
}
|
||||
|
||||
func (ex externalArtifactAdapter) export() any {
|
||||
return exportExternalArtifact(ex.ExternalArtifact)
|
||||
}
|
||||
|
||||
func (ex externalArtifactListAdapter) exportItem(i int) any {
|
||||
return exportExternalArtifact(&ex.ExternalArtifactList.Items[i])
|
||||
}
|
||||
@@ -110,12 +110,6 @@ func TestExport(t *testing.T) {
|
||||
"testdata/export/bucket.yaml",
|
||||
tmpl,
|
||||
},
|
||||
{
|
||||
"source external",
|
||||
"export source external flux-system",
|
||||
"testdata/export/external-artifact.yaml",
|
||||
tmpl,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getArtifactCmd = &cobra.Command{
|
||||
Use: "artifacts",
|
||||
Aliases: []string{"artifact"},
|
||||
Short: "Get artifact object status",
|
||||
Long: `The get artifact sub-commands print the status of artifact objects.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
getCmd.AddCommand(getArtifactCmd)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
)
|
||||
|
||||
var getArtifactGeneratorCmd = &cobra.Command{
|
||||
Use: "generators",
|
||||
Aliases: []string{"generator"},
|
||||
Short: "Get artifact generator statuses",
|
||||
Long: `The get artifact generator command prints the statuses of the resources.`,
|
||||
Example: ` # List all ArtifactGenerators and their status
|
||||
flux get artifact generators`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
get := getCommand{
|
||||
apiType: receiverType,
|
||||
list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}},
|
||||
funcMap: make(typeMap),
|
||||
}
|
||||
|
||||
err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) {
|
||||
o, ok := obj.(*swapi.ArtifactGenerator)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("impossible to cast type %#v generator", obj)
|
||||
}
|
||||
|
||||
sink := artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{
|
||||
Items: []swapi.ArtifactGenerator{
|
||||
*o,
|
||||
}}}
|
||||
return sink, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := get.run(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getArtifactCmd.AddCommand(getArtifactGeneratorCmd)
|
||||
}
|
||||
|
||||
func (s artifactGeneratorListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||
item := s.Items[i]
|
||||
status, msg := statusAndMessage(item.Status.Conditions)
|
||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||
cases.Title(language.English).String(strconv.FormatBool(item.IsDisabled())), status, msg)
|
||||
}
|
||||
|
||||
func (s artifactGeneratorListAdapter) headers(includeNamespace bool) []string {
|
||||
headers := []string{"Name", "Suspended", "Ready", "Message"}
|
||||
if includeNamespace {
|
||||
return append(namespaceHeader, headers...)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func (s artifactGeneratorListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||
item := s.Items[i]
|
||||
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
|
||||
}
|
||||
@@ -28,22 +28,13 @@ import (
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
)
|
||||
|
||||
type getHelmReleaseFlags struct {
|
||||
showSource bool
|
||||
}
|
||||
|
||||
var getHrArgs getHelmReleaseFlags
|
||||
|
||||
var getHelmReleaseCmd = &cobra.Command{
|
||||
Use: "helmreleases",
|
||||
Aliases: []string{"hr", "helmrelease"},
|
||||
Short: "Get HelmRelease statuses",
|
||||
Long: "The get helmreleases command prints the statuses of the resources.",
|
||||
Example: ` # List all Helm releases and their status
|
||||
flux get helmreleases
|
||||
|
||||
# List all Helm releases with source information
|
||||
flux get helmreleases --show-source`,
|
||||
flux get helmreleases`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
get := getCommand{
|
||||
@@ -78,7 +69,6 @@ var getHelmReleaseCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
getHelmReleaseCmd.Flags().BoolVar(&getHrArgs.showSource, "show-source", false, "show the source reference for each helmrelease")
|
||||
getCmd.AddCommand(getHelmReleaseCmd)
|
||||
}
|
||||
|
||||
@@ -89,45 +79,16 @@ func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string {
|
||||
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 {
|
||||
item := a.Items[i]
|
||||
revision := getHelmReleaseRevision(item)
|
||||
status, msg := statusAndMessage(item.Status.Conditions)
|
||||
row := nameColumns(&item, includeNamespace, includeKind)
|
||||
if getHrArgs.showSource {
|
||||
row = append(row, getHelmReleaseSource(item))
|
||||
}
|
||||
return append(row,
|
||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
||||
}
|
||||
|
||||
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {
|
||||
headers := []string{"Name"}
|
||||
if getHrArgs.showSource {
|
||||
headers = append(headers, "Source")
|
||||
}
|
||||
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
|
||||
headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
|
||||
if includeNamespace {
|
||||
headers = append([]string{"Namespace"}, headers...)
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ package main
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var getImageAllCmd = &cobra.Command{
|
||||
Use: "all",
|
||||
Short: "Get all image statuses",
|
||||
Long: "The get image sub-commands print the statuses of all image objects.",
|
||||
Long: withPreviewNote("The get image sub-commands print the statuses of all image objects."),
|
||||
Example: ` # List all image objects in a namespace
|
||||
flux get images all --namespace=flux-system
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var getImagePolicyCmd = &cobra.Command{
|
||||
Use: "policy",
|
||||
Short: "Get ImagePolicy status",
|
||||
Long: "The get image policy command prints the status of ImagePolicy objects.",
|
||||
Long: withPreviewNote("The get image policy command prints the status of ImagePolicy objects."),
|
||||
Example: ` # List all image policies and their status
|
||||
flux get image policy
|
||||
|
||||
@@ -74,16 +74,11 @@ func init() {
|
||||
func (s imagePolicyListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||
item := s.Items[i]
|
||||
status, msg := statusAndMessage(item.Status.Conditions)
|
||||
var image, tag string
|
||||
if ref := item.Status.LatestRef; ref != nil {
|
||||
image = ref.Name
|
||||
tag = ref.Tag
|
||||
}
|
||||
return append(nameColumns(&item, includeNamespace, includeKind), image, tag, status, msg)
|
||||
return append(nameColumns(&item, includeNamespace, includeKind), item.Status.LatestImage, status, msg)
|
||||
}
|
||||
|
||||
func (s imagePolicyListAdapter) headers(includeNamespace bool) []string {
|
||||
headers := []string{"Name", "Image", "Tag", "Ready", "Message"}
|
||||
headers := []string{"Name", "Latest image", "Ready", "Message"}
|
||||
if includeNamespace {
|
||||
return append(namespaceHeader, headers...)
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var getImageRepositoryCmd = &cobra.Command{
|
||||
Use: "repository",
|
||||
Short: "Get ImageRepository status",
|
||||
Long: "The get image repository command prints the status of ImageRepository objects.",
|
||||
Long: withPreviewNote("The get image repository command prints the status of ImageRepository objects."),
|
||||
Example: ` # List all image repositories and their status
|
||||
flux get image repository
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var getImageUpdateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Get ImageUpdateAutomation status",
|
||||
Long: "The get image update command prints the status of ImageUpdateAutomation objects.",
|
||||
Long: withPreviewNote("The get image update command prints the status of ImageUpdateAutomation objects."),
|
||||
Example: ` # List all image update automation object and their status
|
||||
flux get image update
|
||||
|
||||
|
||||
@@ -30,22 +30,13 @@ import (
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
)
|
||||
|
||||
type getKustomizationFlags struct {
|
||||
showSource bool
|
||||
}
|
||||
|
||||
var getKsArgs getKustomizationFlags
|
||||
|
||||
var getKsCmd = &cobra.Command{
|
||||
Use: "kustomizations",
|
||||
Aliases: []string{"ks", "kustomization"},
|
||||
Short: "Get Kustomization statuses",
|
||||
Long: `The get kustomizations command prints the statuses of the resources.`,
|
||||
Example: ` # List all kustomizations and their status
|
||||
flux get kustomizations
|
||||
|
||||
# List all kustomizations with source information
|
||||
flux get kustomizations --show-source`,
|
||||
flux get kustomizations`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
get := getCommand{
|
||||
@@ -83,7 +74,6 @@ var getKsCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
getKsCmd.Flags().BoolVar(&getKsArgs.showSource, "show-source", false, "show the source reference for each kustomization")
|
||||
getCmd.AddCommand(getKsCmd)
|
||||
}
|
||||
|
||||
@@ -93,27 +83,12 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in
|
||||
status, msg := statusAndMessage(item.Status.Conditions)
|
||||
revision = utils.TruncateHex(revision)
|
||||
msg = utils.TruncateHex(msg)
|
||||
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,
|
||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
||||
}
|
||||
|
||||
func (a kustomizationListAdapter) headers(includeNamespace bool) []string {
|
||||
headers := []string{"Name"}
|
||||
if getKsArgs.showSource {
|
||||
headers = append(headers, "Source")
|
||||
}
|
||||
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
|
||||
headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
|
||||
if includeNamespace {
|
||||
headers = append([]string{"Namespace"}, headers...)
|
||||
}
|
||||
|
||||
@@ -59,10 +59,6 @@ var getSourceAllCmd = &cobra.Command{
|
||||
apiType: helmChartType,
|
||||
list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
|
||||
},
|
||||
{
|
||||
apiType: externalArtifactType,
|
||||
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range allSourceCmd {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
)
|
||||
|
||||
var getSourceExternalCmd = &cobra.Command{
|
||||
Use: "external",
|
||||
Short: "Get ExternalArtifact source statuses",
|
||||
Long: `The get sources external command prints the status of the ExternalArtifact sources.`,
|
||||
Example: ` # List all ExternalArtifacts and their status
|
||||
flux get sources external
|
||||
|
||||
# List ExternalArtifacts from all namespaces
|
||||
flux get sources external --all-namespaces`,
|
||||
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
get := getCommand{
|
||||
apiType: externalArtifactType,
|
||||
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
||||
funcMap: make(typeMap),
|
||||
}
|
||||
|
||||
err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) {
|
||||
o, ok := obj.(*sourcev1.ExternalArtifact)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("impossible to cast type %#v to ExternalArtifact", obj)
|
||||
}
|
||||
|
||||
sink := &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{
|
||||
Items: []sourcev1.ExternalArtifact{
|
||||
*o,
|
||||
}}}
|
||||
return sink, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := get.run(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getSourceCmd.AddCommand(getSourceExternalCmd)
|
||||
}
|
||||
|
||||
func (a *externalArtifactListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||
item := a.Items[i]
|
||||
var revision string
|
||||
if item.Status.Artifact != nil {
|
||||
revision = item.Status.Artifact.Revision
|
||||
}
|
||||
status, msg := statusAndMessage(item.Status.Conditions)
|
||||
revision = utils.TruncateHex(revision)
|
||||
msg = utils.TruncateHex(msg)
|
||||
|
||||
var source string
|
||||
if item.Spec.SourceRef != nil {
|
||||
source = fmt.Sprintf("%s/%s/%s",
|
||||
item.Spec.SourceRef.Kind,
|
||||
item.Spec.SourceRef.Namespace,
|
||||
item.Spec.SourceRef.Name)
|
||||
}
|
||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||
revision, source, status, msg)
|
||||
}
|
||||
|
||||
func (a externalArtifactListAdapter) headers(includeNamespace bool) []string {
|
||||
headers := []string{"Name", "Revision", "Source", "Ready", "Message"}
|
||||
if includeNamespace {
|
||||
headers = append([]string{"Namespace"}, headers...)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func (a externalArtifactListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
|
||||
item := a.Items[i]
|
||||
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
|
||||
}
|
||||
@@ -17,12 +17,10 @@ limitations under the License.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
// These are general-purpose adapters for attaching methods to, for
|
||||
@@ -79,34 +77,6 @@ func (a imagePolicyAdapter) asClientObject() client.Object {
|
||||
return a.ImagePolicy
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) deepCopyClientObject() client.Object {
|
||||
return a.ImagePolicy.DeepCopy()
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) isStatic() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) lastHandledReconcileRequest() string {
|
||||
return a.Status.GetLastHandledReconcileRequest()
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) isSuspended() bool {
|
||||
return a.Spec.Suspend
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) setSuspended() {
|
||||
a.Spec.Suspend = true
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) successMessage() string {
|
||||
return fmt.Sprintf("selected ref %s", a.Status.LatestRef.String())
|
||||
}
|
||||
|
||||
func (a imagePolicyAdapter) setUnsuspended() {
|
||||
a.Spec.Suspend = false
|
||||
}
|
||||
|
||||
// imagev1.ImagePolicyList
|
||||
|
||||
type imagePolicyListAdapter struct {
|
||||
@@ -121,18 +91,6 @@ func (a imagePolicyListAdapter) len() int {
|
||||
return len(a.ImagePolicyList.Items)
|
||||
}
|
||||
|
||||
func (a imagePolicyListAdapter) resumeItem(i int) resumable {
|
||||
return &imagePolicyAdapter{&a.ImagePolicyList.Items[i]}
|
||||
}
|
||||
|
||||
func (obj imagePolicyAdapter) getObservedGeneration() int64 {
|
||||
return obj.ImagePolicy.Status.ObservedGeneration
|
||||
}
|
||||
|
||||
func (a imagePolicyListAdapter) item(i int) suspendable {
|
||||
return &imagePolicyAdapter{&a.ImagePolicyList.Items[i]}
|
||||
}
|
||||
|
||||
// autov1.ImageUpdateAutomation
|
||||
|
||||
var imageUpdateAutomationType = apiType{
|
||||
|
||||
@@ -53,18 +53,6 @@ func TestImageScanning(t *testing.T) {
|
||||
"get image policy podinfo-regex",
|
||||
"testdata/image/get_image_policy_regex.golden",
|
||||
},
|
||||
{
|
||||
"suspend image policy podinfo-semver",
|
||||
"testdata/image/suspend_image_policy.golden",
|
||||
},
|
||||
{
|
||||
"resume image policy podinfo-semver",
|
||||
"testdata/image/resume_image_policy.golden",
|
||||
},
|
||||
{
|
||||
"reconcile image policy podinfo-semver",
|
||||
"testdata/image/reconcile_image_policy.golden",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -83,7 +83,7 @@ type installFlags struct {
|
||||
force bool
|
||||
}
|
||||
|
||||
var installArgs = newInstallFlags()
|
||||
var installArgs = NewInstallFlags()
|
||||
|
||||
func init() {
|
||||
installCmd.Flags().BoolVar(&installArgs.export, "export", false,
|
||||
@@ -93,7 +93,7 @@ func init() {
|
||||
installCmd.Flags().StringSliceVar(&installArgs.defaultComponents, "components", rootArgs.defaults.Components,
|
||||
"list of components, accepts comma-separated values")
|
||||
installCmd.Flags().StringSliceVar(&installArgs.extraComponents, "components-extra", nil,
|
||||
"list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller,source-watcher'")
|
||||
"list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller'")
|
||||
installCmd.Flags().StringVar(&installArgs.manifestsPath, "manifests", "", "path to the manifest directory")
|
||||
installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry,
|
||||
"container registry where the toolkit images are published")
|
||||
@@ -115,14 +115,9 @@ func init() {
|
||||
rootCmd.AddCommand(installCmd)
|
||||
}
|
||||
|
||||
func newInstallFlags() installFlags {
|
||||
func NewInstallFlags() installFlags {
|
||||
return installFlags{
|
||||
logLevel: flags.LogLevel(rootArgs.defaults.LogLevel),
|
||||
defaultComponents: rootArgs.defaults.Components,
|
||||
registry: rootArgs.defaults.Registry,
|
||||
watchAllNamespaces: rootArgs.defaults.WatchAllNamespaces,
|
||||
networkPolicy: rootArgs.defaults.NetworkPolicy,
|
||||
clusterDomain: rootArgs.defaults.ClusterDomain,
|
||||
logLevel: flags.LogLevel(rootArgs.defaults.LogLevel),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +195,10 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
if installArgs.export {
|
||||
_, err = rootCmd.OutOrStdout().Write([]byte(manifest.Content))
|
||||
return err
|
||||
fmt.Print(manifest.Content)
|
||||
return nil
|
||||
} else if rootArgs.verbose {
|
||||
_, err = rootCmd.OutOrStdout().Write([]byte(manifest.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(manifest.Content)
|
||||
}
|
||||
|
||||
logger.Successf("manifests build completed")
|
||||
@@ -246,7 +238,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("install failed: %w", err)
|
||||
}
|
||||
|
||||
rootCmd.Println(applyOutput)
|
||||
fmt.Fprintln(os.Stderr, applyOutput)
|
||||
|
||||
if opts.ImagePullSecret != "" && opts.RegistryCredential != "" {
|
||||
logger.Actionf("generating image pull secret %s", opts.ImagePullSecret)
|
||||
@@ -258,7 +250,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
|
||||
Username: credentials[0],
|
||||
Password: credentials[1],
|
||||
}
|
||||
imagePullSecret, err := sourcesecret.GenerateOCI(secretOpts)
|
||||
imagePullSecret, err := sourcesecret.Generate(secretOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("install failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
Copyright 2022 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.
|
||||
@@ -16,17 +16,7 @@ limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
ssautil "github.com/fluxcd/pkg/ssa/utils"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
// The pointer to kubeconfigArgs.Namespace is shared across
|
||||
@@ -69,43 +59,3 @@ func TestInstall(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstall_ComponentsExtra(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
command := "install --export --components-extra=" +
|
||||
strings.Join(install.MakeDefaultOptions().ComponentsExtra, ",")
|
||||
|
||||
output, err := executeCommand(command)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
manifests, err := ssautil.ReadObjects(strings.NewReader(output))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
foundImageAutomation := false
|
||||
foundImageReflector := false
|
||||
foundSourceWatcher := false
|
||||
foundExternalArtifact := false
|
||||
for _, obj := range manifests {
|
||||
if obj.GetKind() == "Deployment" && obj.GetName() == "image-automation-controller" {
|
||||
foundImageAutomation = true
|
||||
}
|
||||
if obj.GetKind() == "Deployment" && obj.GetName() == "image-reflector-controller" {
|
||||
foundImageReflector = true
|
||||
}
|
||||
if obj.GetKind() == "Deployment" && obj.GetName() == "source-watcher" {
|
||||
foundSourceWatcher = true
|
||||
}
|
||||
if obj.GetKind() == "Deployment" &&
|
||||
(obj.GetName() == "kustomize-controller" || obj.GetName() == "helm-controller") {
|
||||
containers, _, _ := unstructured.NestedSlice(obj.Object, "spec", "template", "spec", "containers")
|
||||
g.Expect(containers).ToNot(BeEmpty())
|
||||
args, _, _ := unstructured.NestedSlice(containers[0].(map[string]any), "args")
|
||||
g.Expect(args).To(ContainElement("--feature-gates=ExternalArtifact=true"))
|
||||
foundExternalArtifact = true
|
||||
}
|
||||
}
|
||||
g.Expect(foundImageAutomation).To(BeTrue(), "image-automation-controller deployment not found")
|
||||
g.Expect(foundImageReflector).To(BeTrue(), "image-reflector-controller deployment not found")
|
||||
g.Expect(foundSourceWatcher).To(BeTrue(), "source-watcher deployment not found")
|
||||
g.Expect(foundExternalArtifact).To(BeTrue(), "ExternalArtifact feature gate not found")
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ var listArtifactsCmd = &cobra.Command{
|
||||
Long: `The list command fetches the tags and their metadata from a remote OCI repository.
|
||||
The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`,
|
||||
Example: ` # List the artifacts stored in an OCI repository
|
||||
flux list artifacts oci://ghcr.io/org/config/app
|
||||
flux list artifact oci://ghcr.io/org/config/app
|
||||
`,
|
||||
RunE: listArtifactsCmdRun,
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func listArtifactsCmdRun(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||
logger.Actionf("logging in to registry with provider credentials")
|
||||
ociOpt, _, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String())
|
||||
ociOpt, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during login with provider: %w", err)
|
||||
}
|
||||
|
||||
@@ -100,16 +100,6 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
||||
# Uninstall Flux and delete CRDs
|
||||
flux uninstall`,
|
||||
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")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting namespace: %w", err)
|
||||
@@ -126,11 +116,10 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
||||
var logger = stderrLogger{stderr: os.Stderr}
|
||||
|
||||
type rootFlags struct {
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
pollInterval time.Duration
|
||||
nsFollowsKubeContext bool
|
||||
defaults install.Options
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
pollInterval time.Duration
|
||||
defaults install.Options
|
||||
}
|
||||
|
||||
// RequestError is a custom error type that wraps an error returned by the flux api.
|
||||
@@ -150,8 +139,6 @@ var kubeclientOptions = new(runclient.Options)
|
||||
func init() {
|
||||
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.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()
|
||||
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
|
||||
@@ -193,14 +180,12 @@ func main() {
|
||||
|
||||
// This is required because controller-runtime expects its consumers to
|
||||
// set a logger through log.SetLogger within 30 seconds of the program's
|
||||
// initialization. If not set, the entire debug stack is printed as an
|
||||
// initalization. If not set, the entire debug stack is printed as an
|
||||
// error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59
|
||||
// Since we have our own logging and don't care about controller-runtime's
|
||||
// logger, we configure it's logger to do nothing.
|
||||
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
||||
|
||||
registerPlugins()
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
||||
if err, ok := err.(*RequestError); ok {
|
||||
@@ -218,26 +203,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// getKubeconfigContextNamespace returns the namespace from the current
|
||||
// kubeconfig context, or an empty string if it cannot be determined.
|
||||
func getKubeconfigContextNamespace(cf *genericclioptions.ConfigFlags) string {
|
||||
rawConfig, err := cf.ToRawKubeConfigLoader().RawConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
currentContext := rawConfig.CurrentContext
|
||||
if cf.Context != nil && *cf.Context != "" {
|
||||
currentContext = *cf.Context
|
||||
}
|
||||
|
||||
if ctx, ok := rawConfig.Contexts[currentContext]; ok {
|
||||
return ctx.Namespace
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func configureDefaultNamespace() {
|
||||
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
||||
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
|
||||
@@ -260,9 +225,7 @@ func configureDefaultNamespace() {
|
||||
func readPasswordFromStdin(prompt string) (string, error) {
|
||||
var out string
|
||||
var err error
|
||||
if _, err := fmt.Fprint(os.Stdout, prompt); err != nil {
|
||||
return "", fmt.Errorf("failed to write prompt: %w", err)
|
||||
}
|
||||
fmt.Fprint(os.Stdout, prompt)
|
||||
stdinFD := int(os.Stdin.Fd())
|
||||
if term.IsTerminal(stdinFD) {
|
||||
var inBytes []byte
|
||||
@@ -284,8 +247,3 @@ While we try our best to not introduce breaking changes, they may occur when
|
||||
we adapt to new features and/or find better ways to facilitate what it does.`
|
||||
return fmt.Sprintf("%s\n\n%s", strings.TrimSpace(desc), previewNote)
|
||||
}
|
||||
|
||||
// printlnStdout prints the given text to stdout with a newline.
|
||||
func printlnStdout(txt string) {
|
||||
_, _ = rootCmd.OutOrStdout().Write([]byte(txt + "\n"))
|
||||
}
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
)
|
||||
|
||||
func TestGetKubeconfigContextNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
kubeconfig string
|
||||
context string
|
||||
expectedResult string
|
||||
}{
|
||||
{
|
||||
name: "returns namespace from current context",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: my-context
|
||||
contexts:
|
||||
- name: my-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: custom-ns
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`,
|
||||
expectedResult: "custom-ns",
|
||||
},
|
||||
{
|
||||
name: "returns empty when context has no namespace",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: my-context
|
||||
contexts:
|
||||
- name: my-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`,
|
||||
expectedResult: "",
|
||||
},
|
||||
{
|
||||
name: "returns namespace from context specified via --context flag",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: default-context
|
||||
contexts:
|
||||
- name: default-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: default-ns
|
||||
- name: other-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: other-ns
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`,
|
||||
context: "other-context",
|
||||
expectedResult: "other-ns",
|
||||
},
|
||||
{
|
||||
name: "returns empty when context does not exist",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: non-existent
|
||||
contexts: []
|
||||
clusters: []
|
||||
`,
|
||||
expectedResult: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Write temporary kubeconfig.
|
||||
tmpDir := t.TempDir()
|
||||
kcPath := filepath.Join(tmpDir, "kubeconfig")
|
||||
g.Expect(os.WriteFile(kcPath, []byte(tt.kubeconfig), 0o600)).To(Succeed())
|
||||
|
||||
// Use a local ConfigFlags instance to avoid polluting the
|
||||
// package-global kubeconfigArgs (which caches a clientConfig
|
||||
// internally and would leak state across tests).
|
||||
cf := genericclioptions.NewConfigFlags(false)
|
||||
cf.KubeConfig = &kcPath
|
||||
cf.Context = &tt.context
|
||||
|
||||
got := getKubeconfigContextNamespace(cf)
|
||||
g.Expect(got).To(Equal(tt.expectedResult))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextNamespaceOptIn(t *testing.T) {
|
||||
kubeconfig := `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: my-context
|
||||
contexts:
|
||||
- name: my-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: context-ns
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
nsFollowsFlag bool
|
||||
nsFollowsEnv string
|
||||
envNamespace string
|
||||
flagNamespace string
|
||||
expectedNamespace string
|
||||
}{
|
||||
{
|
||||
name: "ignores context namespace when not opted in",
|
||||
expectedNamespace: rootArgs.defaults.Namespace,
|
||||
},
|
||||
{
|
||||
name: "uses context namespace when opted in via flag",
|
||||
nsFollowsFlag: true,
|
||||
expectedNamespace: "context-ns",
|
||||
},
|
||||
{
|
||||
name: "uses context namespace when opted in via env var",
|
||||
nsFollowsEnv: "1",
|
||||
expectedNamespace: "context-ns",
|
||||
},
|
||||
{
|
||||
name: "context namespace takes precedence over FLUX_SYSTEM_NAMESPACE when opted in",
|
||||
nsFollowsFlag: true,
|
||||
envNamespace: "env-ns",
|
||||
expectedNamespace: "context-ns",
|
||||
},
|
||||
{
|
||||
name: "FLUX_SYSTEM_NAMESPACE used when not opted in",
|
||||
envNamespace: "env-ns",
|
||||
expectedNamespace: "env-ns",
|
||||
},
|
||||
{
|
||||
name: "--namespace flag takes precedence over context namespace",
|
||||
nsFollowsFlag: true,
|
||||
flagNamespace: "flag-ns",
|
||||
expectedNamespace: "flag-ns",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Write temporary kubeconfig.
|
||||
tmpDir := t.TempDir()
|
||||
kcPath := filepath.Join(tmpDir, "kubeconfig")
|
||||
g.Expect(os.WriteFile(kcPath, []byte(kubeconfig), 0o600)).To(Succeed())
|
||||
|
||||
// Use a local ConfigFlags instance to avoid polluting the
|
||||
// package-global kubeconfigArgs.
|
||||
cf := genericclioptions.NewConfigFlags(false)
|
||||
cf.KubeConfig = &kcPath
|
||||
emptyCtx := ""
|
||||
cf.Context = &emptyCtx
|
||||
|
||||
// Mirror configureDefaultNamespace behavior on the local instance.
|
||||
defaultNs := rootArgs.defaults.Namespace
|
||||
cf.Namespace = &defaultNs
|
||||
|
||||
if tt.envNamespace != "" {
|
||||
t.Setenv("FLUX_SYSTEM_NAMESPACE", tt.envNamespace)
|
||||
envNs := tt.envNamespace
|
||||
cf.Namespace = &envNs
|
||||
}
|
||||
if tt.nsFollowsEnv != "" {
|
||||
t.Setenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT", tt.nsFollowsEnv)
|
||||
}
|
||||
|
||||
// Simulate PersistentPreRunE behavior.
|
||||
if tt.flagNamespace != "" {
|
||||
*cf.Namespace = tt.flagNamespace
|
||||
} else if tt.nsFollowsFlag || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "" {
|
||||
if ctxNs := getKubeconfigContextNamespace(cf); ctxNs != "" {
|
||||
*cf.Namespace = ctxNs
|
||||
}
|
||||
}
|
||||
|
||||
g.Expect(*cf.Namespace).To(Equal(tt.expectedNamespace))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -374,12 +374,6 @@ func executeCommand(cmd string) (string, error) {
|
||||
// in subsequent executions which causes tests to fail that rely on the value
|
||||
// of "Changed".
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -453,16 +447,13 @@ func resetCmdArgs() {
|
||||
imagePolicyArgs = imagePolicyFlags{}
|
||||
imageRepoArgs = imageRepoFlags{}
|
||||
imageUpdateArgs = imageUpdateFlags{}
|
||||
installArgs = newInstallFlags()
|
||||
kustomizationArgs = NewKustomizationFlags()
|
||||
receiverArgs = receiverFlags{}
|
||||
resumeArgs = ResumeFlags{}
|
||||
rhrArgs = reconcileHelmReleaseFlags{}
|
||||
rksArgs = reconcileKsFlags{}
|
||||
secretGitArgs = NewSecretGitFlags()
|
||||
secretGitHubAppArgs = secretGitHubAppFlags{}
|
||||
secretProxyArgs = secretProxyFlags{}
|
||||
secretReceiverArgs = secretReceiverFlags{}
|
||||
secretHelmArgs = secretHelmFlags{}
|
||||
secretTLSArgs = secretTLSFlags{}
|
||||
sourceBucketArgs = sourceBucketFlags{}
|
||||
|
||||
@@ -1,702 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fluxcd/pkg/ssa"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
imageautov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||
imageautov1b2 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
||||
imagev1b2 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
swv1b1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||
)
|
||||
|
||||
// APIVersions holds the mapping of GroupKinds to their respective
|
||||
// latest API versions for a specific Flux version.
|
||||
type APIVersions struct {
|
||||
FluxVersion string
|
||||
LatestVersions map[schema.GroupKind]string
|
||||
}
|
||||
|
||||
// TODO: Update this mapping when new Flux minor versions are released!
|
||||
// latestAPIVersions contains the latest API versions for each GroupKind
|
||||
// for each supported Flux version. The number of latest minor versions
|
||||
// we maintain here must match what's documented here:
|
||||
//
|
||||
// https://fluxcd.io/flux/releases/#supported-releases
|
||||
var latestAPIVersions = []APIVersions{
|
||||
{
|
||||
FluxVersion: "2.8",
|
||||
LatestVersions: flux27LatestAPIVersions,
|
||||
},
|
||||
{
|
||||
FluxVersion: "2.7",
|
||||
LatestVersions: flux27LatestAPIVersions,
|
||||
},
|
||||
{
|
||||
FluxVersion: "2.6",
|
||||
LatestVersions: flux26LatestAPIVersions,
|
||||
},
|
||||
}
|
||||
|
||||
var flux27LatestAPIVersions = map[schema.GroupKind]string{
|
||||
// source-controller
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||
|
||||
// kustomize-controller
|
||||
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||
|
||||
// helm-controller
|
||||
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||
|
||||
// notification-controller
|
||||
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||
|
||||
// image-reflector-controller
|
||||
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImageRepositoryKind}: imagev1.GroupVersion.Version,
|
||||
{Group: imagev1.GroupVersion.Group, Kind: imagev1.ImagePolicyKind}: imagev1.GroupVersion.Version,
|
||||
|
||||
// image-automation-controller
|
||||
{Group: imageautov1.GroupVersion.Group, Kind: imageautov1.ImageUpdateAutomationKind}: imageautov1.GroupVersion.Version,
|
||||
|
||||
// source-watcher
|
||||
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
||||
}
|
||||
|
||||
var flux26LatestAPIVersions = map[schema.GroupKind]string{
|
||||
// source-controller
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version,
|
||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version,
|
||||
|
||||
// kustomize-controller
|
||||
{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version,
|
||||
|
||||
// helm-controller
|
||||
{Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version,
|
||||
|
||||
// notification-controller
|
||||
{Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version,
|
||||
{Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version,
|
||||
|
||||
// image-reflector-controller
|
||||
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImageRepositoryKind}: imagev1b2.GroupVersion.Version,
|
||||
{Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImagePolicyKind}: imagev1b2.GroupVersion.Version,
|
||||
|
||||
// image-automation-controller
|
||||
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
||||
}
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Args: cobra.NoArgs,
|
||||
Short: "Migrate the Flux custom resources to their latest API version",
|
||||
Long: `The migrate command must be run before a Flux minor version upgrade.
|
||||
|
||||
The command has two modes of operation:
|
||||
|
||||
- Cluster mode (default): migrates all the Flux custom resources stored in Kubernetes etcd to their latest API version.
|
||||
- File system mode (-f): migrates the Flux custom resources defined in the manifests located in the specified path.
|
||||
`,
|
||||
Example: ` # Migrate all the Flux custom resources in the cluster.
|
||||
# This uses the current kubeconfig context and requires cluster-admin permissions.
|
||||
flux migrate
|
||||
|
||||
# Migrate all the Flux custom resources in a Git repository
|
||||
# checked out in the current working directory.
|
||||
flux migrate -f .
|
||||
|
||||
# Migrate all Flux custom resources defined in YAML and Helm YAML template files.
|
||||
flux migrate -f . --extensions=.yml,.yaml,.tpl
|
||||
|
||||
# Migrate the Flux custom resources to the latest API versions of Flux 2.6.
|
||||
flux migrate -f . --version=2.6
|
||||
|
||||
# Migrate the Flux custom resources defined in a multi-document YAML manifest file.
|
||||
flux migrate -f path/to/manifest.yaml
|
||||
|
||||
# Simulate the migration without making any changes.
|
||||
flux migrate -f . --dry-run
|
||||
|
||||
# Run the migration skipping confirmation prompts.
|
||||
flux migrate -f . --yes
|
||||
`,
|
||||
RunE: runMigrateCmd,
|
||||
}
|
||||
|
||||
var migrateFlags struct {
|
||||
yes bool
|
||||
dryRun bool
|
||||
path string
|
||||
version string
|
||||
extensions []string
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(migrateCmd)
|
||||
|
||||
migrateCmd.Flags().StringVarP(&migrateFlags.path, "path", "f", "",
|
||||
"the path to the directory containing the manifests to migrate")
|
||||
migrateCmd.Flags().StringSliceVarP(&migrateFlags.extensions, "extensions", "e", []string{".yaml", ".yml"},
|
||||
"the file extensions to consider when migrating manifests, only applicable with --path")
|
||||
migrateCmd.Flags().StringVarP(&migrateFlags.version, "version", "v", "",
|
||||
"the target Flux minor version to migrate manifests to, only applicable with --path (defaults to the version of the CLI)")
|
||||
migrateCmd.Flags().BoolVarP(&migrateFlags.yes, "yes", "y", false,
|
||||
"skip confirmation prompts when migrating manifests, only applicable with --path")
|
||||
migrateCmd.Flags().BoolVar(&migrateFlags.dryRun, "dry-run", false,
|
||||
"simulate the migration of manifests without making any changes, only applicable with --path")
|
||||
}
|
||||
|
||||
func runMigrateCmd(*cobra.Command, []string) error {
|
||||
if migrateFlags.path == "" {
|
||||
return migrateCluster()
|
||||
}
|
||||
return migrateFileSystem()
|
||||
}
|
||||
|
||||
func migrateCluster() error {
|
||||
logger.Actionf("starting migration of custom resources")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||
defer cancel()
|
||||
|
||||
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the Kubernetes client initialization failed: %w", err)
|
||||
}
|
||||
|
||||
kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrator := NewClusterMigrator(kubeClient, client.MatchingLabels{
|
||||
"app.kubernetes.io/part-of": "flux",
|
||||
})
|
||||
|
||||
if err := migrator.Run(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Successf("custom resources migrated successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateFileSystem() error {
|
||||
pathRoot, err := os.OpenRoot(".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open filesystem at the current working directory: %w", err)
|
||||
}
|
||||
defer pathRoot.Close()
|
||||
|
||||
fileSystem := &osFS{pathRoot.FS()}
|
||||
yes := migrateFlags.yes
|
||||
dryRun := migrateFlags.dryRun
|
||||
path := migrateFlags.path
|
||||
extensions := migrateFlags.extensions
|
||||
var latestVersions map[schema.GroupKind]string
|
||||
|
||||
// Determine latest API versions based on the Flux version.
|
||||
if migrateFlags.version == "" {
|
||||
latestVersions = latestAPIVersions[0].LatestVersions
|
||||
} else {
|
||||
supportedVersions := make([]string, 0, len(latestAPIVersions))
|
||||
for _, v := range latestAPIVersions {
|
||||
if v.FluxVersion == migrateFlags.version {
|
||||
latestVersions = v.LatestVersions
|
||||
break
|
||||
}
|
||||
supportedVersions = append(supportedVersions, v.FluxVersion)
|
||||
}
|
||||
if latestVersions == nil {
|
||||
return fmt.Errorf("version %s is not supported, supported versions are: %s",
|
||||
migrateFlags.version, strings.Join(supportedVersions, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return NewFileSystemMigrator(fileSystem, yes, dryRun, path, extensions, latestVersions).Run()
|
||||
}
|
||||
|
||||
// ClusterMigrator migrates all the CRs in the cluster for the CRDs matching the label selector.
|
||||
type ClusterMigrator struct {
|
||||
labelSelector client.MatchingLabels
|
||||
kubeClient client.Client
|
||||
}
|
||||
|
||||
// NewClusterMigrator creates a new ClusterMigrator instance with the specified label selector.
|
||||
func NewClusterMigrator(kubeClient client.Client, labelSelector client.MatchingLabels) *ClusterMigrator {
|
||||
return &ClusterMigrator{
|
||||
labelSelector: labelSelector,
|
||||
kubeClient: kubeClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClusterMigrator) Run(ctx context.Context) error {
|
||||
crdList := &apiextensionsv1.CustomResourceDefinitionList{}
|
||||
|
||||
if err := c.kubeClient.List(ctx, crdList, c.labelSelector); err != nil {
|
||||
return fmt.Errorf("failed to list CRDs: %w", err)
|
||||
}
|
||||
|
||||
for _, crd := range crdList.Items {
|
||||
if err := c.migrateCRD(ctx, crd.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterMigrator) migrateCRD(ctx context.Context, name string) error {
|
||||
crd := &apiextensionsv1.CustomResourceDefinition{}
|
||||
|
||||
if err := c.kubeClient.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil {
|
||||
return fmt.Errorf("failed to get CRD %s: %w", name, err)
|
||||
}
|
||||
|
||||
// get the latest storage version for the CRD
|
||||
storageVersion := c.getStorageVersion(crd)
|
||||
if storageVersion == "" {
|
||||
return fmt.Errorf("no storage version found for CRD %s", name)
|
||||
}
|
||||
|
||||
// migrate all the resources for the CRD
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
return c.migrateCR(ctx, crd, storageVersion)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate resources for CRD %s: %w", name, err)
|
||||
}
|
||||
|
||||
// set the CRD status to contain only the latest storage version
|
||||
if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != storageVersion {
|
||||
crd.Status.StoredVersions = []string{storageVersion}
|
||||
if err := c.kubeClient.Status().Update(ctx, crd); err != nil {
|
||||
return fmt.Errorf("failed to update CRD %s status: %w", crd.Name, err)
|
||||
}
|
||||
logger.Successf("%s migrated to storage version %s", crd.Name, storageVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateCR migrates all CRs for the given CRD to the specified version by patching them.
|
||||
func (c *ClusterMigrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, version string) error {
|
||||
list := &unstructured.UnstructuredList{}
|
||||
|
||||
apiVersion := crd.Spec.Group + "/" + version
|
||||
listKind := crd.Spec.Names.ListKind
|
||||
|
||||
list.SetAPIVersion(apiVersion)
|
||||
list.SetKind(listKind)
|
||||
|
||||
err := c.kubeClient.List(ctx, list, client.InNamespace(""))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list resources for CRD %s: %w", crd.Name, err)
|
||||
}
|
||||
|
||||
if len(list.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range list.Items {
|
||||
patches, err := ssa.PatchMigrateToVersion(&item, apiVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration patch for %s/%s/%s: %w",
|
||||
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||
}
|
||||
|
||||
if len(patches) == 0 {
|
||||
// patch the resource with an empty patch to update the version
|
||||
if err := c.kubeClient.Patch(
|
||||
ctx,
|
||||
&item,
|
||||
client.RawPatch(client.Merge.Type(), []byte("{}")),
|
||||
); err != nil && !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf(" %s/%s/%s failed to migrate: %w",
|
||||
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||
}
|
||||
} else {
|
||||
// patch the resource to migrate the managed fields to the latest apiVersion
|
||||
rawPatch, err := json.Marshal(patches)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal migration patch for %s/%s/%s: %w",
|
||||
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||
}
|
||||
if err := c.kubeClient.Patch(
|
||||
ctx,
|
||||
&item,
|
||||
client.RawPatch(types.JSONPatchType, rawPatch),
|
||||
); err != nil && !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf(" %s/%s/%s failed to migrate managed fields: %w",
|
||||
item.GetKind(), item.GetNamespace(), item.GetName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Successf("%s/%s/%s migrated to version %s",
|
||||
item.GetKind(), item.GetNamespace(), item.GetName(), version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStorageVersion retrieves the storage version of a CustomResourceDefinition.
|
||||
func (c *ClusterMigrator) getStorageVersion(crd *apiextensionsv1.CustomResourceDefinition) string {
|
||||
var version string
|
||||
for _, v := range crd.Spec.Versions {
|
||||
if v.Storage {
|
||||
version = v.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
// WritableFS extends fs.FS with a WriteFile method.
|
||||
type WritableFS interface {
|
||||
fs.FS
|
||||
WriteFile(name string, data []byte, perm os.FileMode) error
|
||||
}
|
||||
|
||||
// osFS is a WritableFS implementation that uses the file system of the OS.
|
||||
type osFS struct {
|
||||
fs.FS
|
||||
}
|
||||
|
||||
func (o *osFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||
return os.WriteFile(name, data, perm)
|
||||
}
|
||||
|
||||
// FileSystemMigrator migrates all the CRs found in the manifests located in the specified path.
|
||||
type FileSystemMigrator struct {
|
||||
fileSystem WritableFS
|
||||
yes bool
|
||||
dryRun bool
|
||||
path string
|
||||
extensions []string
|
||||
latestVersions map[schema.GroupKind]string
|
||||
}
|
||||
|
||||
// FileAPIUpgrades represents the API upgrades detected in a specific manifest file.
|
||||
type FileAPIUpgrades struct {
|
||||
File string
|
||||
Upgrades []APIUpgrade
|
||||
}
|
||||
|
||||
// APIUpgrade represents an upgrade of a specific API version in a manifest file.
|
||||
type APIUpgrade struct {
|
||||
Line int
|
||||
Kind string
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
}
|
||||
|
||||
// NewFileSystemMigrator creates a new FileSystemMigrator instance with the specified flags.
|
||||
func NewFileSystemMigrator(fileSystem WritableFS, yes, dryRun bool, path string,
|
||||
extensions []string, latestVersions map[schema.GroupKind]string) *FileSystemMigrator {
|
||||
return &FileSystemMigrator{
|
||||
fileSystem: fileSystem,
|
||||
yes: yes,
|
||||
dryRun: dryRun,
|
||||
path: filepath.Clean(path), // convert dir/ to dir to avoid error when walking
|
||||
extensions: extensions,
|
||||
latestVersions: latestVersions,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) Run() error {
|
||||
logger.Actionf("starting migration of custom resources")
|
||||
|
||||
// List and filter files.
|
||||
files, err := f.listFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Detect upgrades.
|
||||
upgrades, err := f.detectUpgrades(files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(upgrades) == 0 {
|
||||
logger.Successf("no custom resources found that require migration")
|
||||
return nil
|
||||
}
|
||||
if f.dryRun {
|
||||
logger.Successf("dry-run mode enabled, no changes will be made")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Confirm upgrades.
|
||||
if !f.yes {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Are you sure you want to proceed with the above upgrades", // Already prints "? [y/N]"
|
||||
IsConfirm: true,
|
||||
}
|
||||
if _, err := prompt.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate files.
|
||||
for _, fileUpgrades := range upgrades {
|
||||
if err := f.migrateFile(&fileUpgrades); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Successf("file %s migrated successfully", fileUpgrades.File)
|
||||
}
|
||||
|
||||
logger.Successf("custom resources migrated successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) listFiles() ([]string, error) {
|
||||
fileInfo, err := fs.Stat(f.fileSystem, f.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat path %s: %w", f.path, err)
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return f.listDirectoryFiles()
|
||||
}
|
||||
if err := f.validateSingleFile(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string{f.path}, nil
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) listDirectoryFiles() ([]string, error) {
|
||||
var files []string
|
||||
err := fs.WalkDir(f.fileSystem, f.path, func(path string, dirEntry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !f.matchesExtensions(path) {
|
||||
return nil
|
||||
}
|
||||
fileInfo, err := dirEntry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.Mode().IsRegular() {
|
||||
files = append(files, path)
|
||||
} else if !fileInfo.IsDir() {
|
||||
logger.Warningf("skipping irregular file %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to walk directory %s: %w", f.path, err)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) validateSingleFile() error {
|
||||
if !f.matchesExtensions(f.path) {
|
||||
return fmt.Errorf("file %s does not match the specified extensions: %v",
|
||||
f.path, strings.Join(f.extensions, ", "))
|
||||
}
|
||||
|
||||
// Check if it's irregular by walking the parent directory.
|
||||
var irregular bool
|
||||
err := fs.WalkDir(f.fileSystem, filepath.Dir(f.path), func(path string, dirEntry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path != f.path {
|
||||
return nil
|
||||
}
|
||||
fileInfo, err := dirEntry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fileInfo.Mode().IsRegular() {
|
||||
irregular = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate file %s: %w", f.path, err)
|
||||
}
|
||||
if irregular {
|
||||
return fmt.Errorf("file %s is irregular", f.path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) matchesExtensions(file string) bool {
|
||||
for _, ext := range f.extensions {
|
||||
if strings.HasSuffix(file, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) detectUpgrades(files []string) ([]FileAPIUpgrades, error) {
|
||||
var upgrades []FileAPIUpgrades
|
||||
for _, file := range files {
|
||||
fileUpgrades, err := f.detectFileUpgrades(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fileUpgrades) == 0 {
|
||||
continue
|
||||
}
|
||||
fu := FileAPIUpgrades{
|
||||
File: file,
|
||||
Upgrades: fileUpgrades,
|
||||
}
|
||||
upgrades = append(upgrades, fu)
|
||||
f.printDetectedUpgrades(&fu)
|
||||
}
|
||||
return upgrades, nil
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) detectFileUpgrades(file string) ([]APIUpgrade, error) {
|
||||
b, err := fs.ReadFile(f.fileSystem, file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file %s: %w", file, err)
|
||||
}
|
||||
lines := strings.Split(string(b), "\n")
|
||||
|
||||
var fileUpgrades []APIUpgrade
|
||||
for line, apiVersionLine := range lines {
|
||||
// Parse apiVersion.
|
||||
const apiVersionPrefix = "apiVersion: "
|
||||
idx := strings.Index(apiVersionLine, apiVersionPrefix)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
apiVersionValuePrefix := strings.TrimSpace(apiVersionLine[idx+len(apiVersionPrefix):])
|
||||
apiVersion := strings.Split(apiVersionValuePrefix, " ")[0]
|
||||
gv, err := schema.ParseGroupVersion(apiVersion)
|
||||
if err != nil {
|
||||
logger.Warningf("%s:%d: %v", file, line+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse kind.
|
||||
if line+1 >= len(lines) {
|
||||
continue
|
||||
}
|
||||
kindLine := lines[line+1]
|
||||
const kindPrefix = "kind: "
|
||||
idx = strings.Index(kindLine, kindPrefix)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
kindValuePrefix := strings.TrimSpace(kindLine[idx+len(kindPrefix):])
|
||||
kind := strings.Split(kindValuePrefix, " ")[0]
|
||||
|
||||
// Build GroupKind.
|
||||
gk := schema.GroupKind{
|
||||
Group: gv.Group,
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
// Check if there's a newer version for the GroupKind.
|
||||
latestVersion, ok := f.latestVersions[gk]
|
||||
if !ok || latestVersion == gv.Version {
|
||||
continue
|
||||
}
|
||||
|
||||
// Record the upgrade.
|
||||
fileUpgrades = append(fileUpgrades, APIUpgrade{
|
||||
Line: line,
|
||||
Kind: kind,
|
||||
OldVersion: gv.Version,
|
||||
NewVersion: latestVersion,
|
||||
})
|
||||
}
|
||||
return fileUpgrades, nil
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) printDetectedUpgrades(fileUpgrades *FileAPIUpgrades) {
|
||||
for _, upgrade := range fileUpgrades.Upgrades {
|
||||
logger.Generatef("%s:%d: %s %s -> %s",
|
||||
fileUpgrades.File,
|
||||
upgrade.Line+1,
|
||||
upgrade.Kind,
|
||||
upgrade.OldVersion,
|
||||
upgrade.NewVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileSystemMigrator) migrateFile(fileUpgrades *FileAPIUpgrades) error {
|
||||
// Read file and map lines.
|
||||
b, err := fs.ReadFile(f.fileSystem, fileUpgrades.File)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", fileUpgrades.File, err)
|
||||
}
|
||||
lines := strings.Split(string(b), "\n")
|
||||
|
||||
// Apply upgrades to lines.
|
||||
for _, upgrade := range fileUpgrades.Upgrades {
|
||||
line := lines[upgrade.Line]
|
||||
line = strings.Replace(line, upgrade.OldVersion, upgrade.NewVersion, 1)
|
||||
lines[upgrade.Line] = line
|
||||
}
|
||||
|
||||
// Read file info to preserve permissions.
|
||||
fileInfo, err := fs.Stat(f.fileSystem, fileUpgrades.File)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat file %s: %w", fileUpgrades.File, err)
|
||||
}
|
||||
|
||||
// Write file with preserved permissions.
|
||||
b = []byte(strings.Join(lines, "\n"))
|
||||
if err := f.fileSystem.WriteFile(fileUpgrades.File, b, fileInfo.Mode()); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", fileUpgrades.File, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type writeToMemoryFS struct {
|
||||
fs.FS
|
||||
|
||||
writtenFiles map[string][]byte
|
||||
}
|
||||
|
||||
func (m *writeToMemoryFS) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||
m.writtenFiles[name] = data
|
||||
return nil
|
||||
}
|
||||
|
||||
type writtenFile struct {
|
||||
file string
|
||||
goldenFile string
|
||||
}
|
||||
|
||||
func TestFileSystemMigrator(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
path string
|
||||
outputGolden string
|
||||
writtenFiles []writtenFile
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "errors out for single file that is a symlink",
|
||||
path: "testdata/migrate/file-system/single-file-link.yaml",
|
||||
err: "file testdata/migrate/file-system/single-file-link.yaml is irregular",
|
||||
},
|
||||
{
|
||||
name: "errors out for single file with wrong extension",
|
||||
path: "testdata/migrate/file-system/single-file-wrong-ext.json",
|
||||
err: "file testdata/migrate/file-system/single-file-wrong-ext.json does not match the specified extensions: .yaml, .yml",
|
||||
},
|
||||
{
|
||||
name: "migrate single file",
|
||||
path: "testdata/migrate/file-system/single-file.yaml",
|
||||
outputGolden: "testdata/migrate/file-system/single-file.yaml.output.golden",
|
||||
writtenFiles: []writtenFile{
|
||||
{
|
||||
file: "testdata/migrate/file-system/single-file.yaml",
|
||||
goldenFile: "testdata/migrate/file-system/single-file.yaml.golden",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "migrate files in directory",
|
||||
path: "testdata/migrate/file-system/dir",
|
||||
outputGolden: "testdata/migrate/file-system/dir.output.golden",
|
||||
writtenFiles: []writtenFile{
|
||||
{
|
||||
file: "testdata/migrate/file-system/dir/some-dir/another-file.yaml",
|
||||
goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yaml",
|
||||
},
|
||||
{
|
||||
file: "testdata/migrate/file-system/dir/some-dir/another-file.yml",
|
||||
goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yml",
|
||||
},
|
||||
{
|
||||
file: "testdata/migrate/file-system/dir/some-file.yaml",
|
||||
goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yaml",
|
||||
},
|
||||
{
|
||||
file: "testdata/migrate/file-system/dir/some-file.yml",
|
||||
goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yml",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Store logger, replace with test logger, and restore at the end of the test.
|
||||
var testLogger bytes.Buffer
|
||||
oldLogger := logger
|
||||
logger = stderrLogger{&testLogger}
|
||||
t.Cleanup(func() { logger = oldLogger })
|
||||
|
||||
// Open current working directory as root and build write-to-memory filesystem.
|
||||
pathRoot, err := os.OpenRoot(".")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
t.Cleanup(func() { pathRoot.Close() })
|
||||
fileSystem := &writeToMemoryFS{
|
||||
FS: pathRoot.FS(),
|
||||
writtenFiles: make(map[string][]byte),
|
||||
}
|
||||
|
||||
// Prepare other inputs.
|
||||
const yes = true
|
||||
const dryRun = false
|
||||
extensions := []string{".yaml", ".yml"}
|
||||
latestVersions := map[schema.GroupKind]string{
|
||||
{Group: "image.toolkit.fluxcd.io", Kind: "ImageRepository"}: "v1",
|
||||
{Group: "image.toolkit.fluxcd.io", Kind: "ImagePolicy"}: "v1",
|
||||
{Group: "image.toolkit.fluxcd.io", Kind: "ImageUpdateAutomation"}: "v1",
|
||||
}
|
||||
|
||||
// Run migration.
|
||||
err = NewFileSystemMigrator(fileSystem, yes, dryRun, tt.path, extensions, latestVersions).Run()
|
||||
if tt.err != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal(tt.err))
|
||||
return
|
||||
}
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Assert logger output.
|
||||
b, err := os.ReadFile(tt.outputGolden)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(string(b)).To(Equal(testLogger.String()),
|
||||
"logger output does not match golden file %s", tt.outputGolden)
|
||||
|
||||
// Assert which files were written.
|
||||
writtenFiles := make([]string, 0, len(fileSystem.writtenFiles))
|
||||
for name := range fileSystem.writtenFiles {
|
||||
writtenFiles = append(writtenFiles, name)
|
||||
}
|
||||
expectedWrittenFiles := make([]string, 0, len(tt.writtenFiles))
|
||||
for _, wf := range tt.writtenFiles {
|
||||
expectedWrittenFiles = append(expectedWrittenFiles, wf.file)
|
||||
}
|
||||
g.Expect(writtenFiles).To(ConsistOf(expectedWrittenFiles))
|
||||
|
||||
// Assert contents of written files.
|
||||
for _, wf := range tt.writtenFiles {
|
||||
b, err := os.ReadFile(wf.goldenFile)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(string(fileSystem.writtenFiles[wf.file])).To(Equal(string(b)),
|
||||
"file %s does not match golden file %s", wf.file, wf.goldenFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
@@ -29,14 +29,17 @@ import (
|
||||
)
|
||||
|
||||
// loginWithProvider gets a crane authentication option for the given provider and URL.
|
||||
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, authn.Authenticator, error) {
|
||||
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, error) {
|
||||
var opts []auth.Option
|
||||
if provider == azure.ProviderName {
|
||||
opts = append(opts, auth.WithAllowShellOut())
|
||||
}
|
||||
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err)
|
||||
return nil, fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err)
|
||||
}
|
||||
return crane.WithAuth(authenticator), authenticator, nil
|
||||
if authenticator == nil {
|
||||
return nil, errors.New("unsupported provider")
|
||||
}
|
||||
return crane.WithAuth(authenticator), nil
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||
)
|
||||
|
||||
var pluginHandler = plugin.NewHandler()
|
||||
|
||||
var pluginCmd = &cobra.Command{
|
||||
Use: "plugin",
|
||||
Short: "Manage Flux CLI plugins",
|
||||
Long: `The plugin sub-commands manage Flux CLI plugins.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// No-op: skip root's namespace DNS validation for plugin commands.
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pluginCmd)
|
||||
}
|
||||
|
||||
// builtinCommandNames returns the names of all non-plugin commands on rootCmd.
|
||||
func builtinCommandNames() []string {
|
||||
var names []string
|
||||
for _, c := range rootCmd.Commands() {
|
||||
if c.GroupID != "plugin" {
|
||||
names = append(names, c.Name())
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// registerPlugins scans the plugin directory and registers discovered
|
||||
// plugins as Cobra subcommands on rootCmd.
|
||||
func registerPlugins() {
|
||||
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||
if len(plugins) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !rootCmd.ContainsGroup("plugin") {
|
||||
rootCmd.AddGroup(&cobra.Group{
|
||||
ID: "plugin",
|
||||
Title: "Plugin Commands:",
|
||||
})
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
cmd := &cobra.Command{
|
||||
Use: p.Name,
|
||||
Short: fmt.Sprintf("Runs the %s plugin", p.Name),
|
||||
Long: fmt.Sprintf("This command runs the %s plugin.\nUse 'flux %s --help' for full plugin help.", p.Name, p.Name),
|
||||
DisableFlagParsing: true,
|
||||
GroupID: "plugin",
|
||||
ValidArgsFunction: plugin.CompleteFunc(p.Path),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return plugin.Exec(p.Path, args)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// parseNameVersion splits "operator@0.45.0" into ("operator", "0.45.0").
|
||||
// If no @ is present, version is empty (latest).
|
||||
func parseNameVersion(s string) (string, string) {
|
||||
name, version, found := strings.Cut(s, "@")
|
||||
if found {
|
||||
return name, version
|
||||
}
|
||||
return s, ""
|
||||
}
|
||||
|
||||
// isDigestRef reports whether ref is a content-addressable digest
|
||||
// (e.g. "sha256:06e0a38...").
|
||||
func isDigestRef(ref string) bool {
|
||||
return strings.HasPrefix(ref, "sha256:")
|
||||
}
|
||||
|
||||
// newCatalogClient creates a CatalogClient that respects FLUXCD_PLUGIN_CATALOG.
|
||||
func newCatalogClient() *plugin.CatalogClient {
|
||||
client := plugin.NewCatalogClient()
|
||||
client.GetEnv = pluginHandler.GetEnv
|
||||
return client
|
||||
}
|
||||
|
||||
func newPluginSpinner(message string) *spinner.Spinner {
|
||||
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||
s.Suffix = " " + message
|
||||
return s
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||
)
|
||||
|
||||
var pluginInstallCmd = &cobra.Command{
|
||||
Use: "install <name>[@<version>|@<digest>]",
|
||||
Short: "Install a plugin from the catalog",
|
||||
Long: `The plugin install command downloads and installs a plugin from the Flux plugin catalog.
|
||||
|
||||
Examples:
|
||||
# Install the latest version
|
||||
flux plugin install operator
|
||||
|
||||
# Install a specific version
|
||||
flux plugin install operator@0.45.0
|
||||
|
||||
# Install pinned to a specific digest
|
||||
flux plugin install operator@sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: pluginInstallCmdRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
pluginCmd.AddCommand(pluginInstallCmd)
|
||||
}
|
||||
|
||||
func pluginInstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||
nameVersion := args[0]
|
||||
name, ref := parseNameVersion(nameVersion)
|
||||
|
||||
catalogClient := newCatalogClient()
|
||||
manifest, err := catalogClient.FetchManifest(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pv *plugintypes.Version
|
||||
var plat *plugintypes.Platform
|
||||
|
||||
if isDigestRef(ref) {
|
||||
dm, err := plugin.ResolveByDigest(manifest, ref, runtime.GOOS, runtime.GOARCH)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pv = dm.Version
|
||||
plat = dm.Platform
|
||||
} else {
|
||||
pv, err = plugin.ResolveVersion(manifest, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plat, err = plugin.ResolvePlatform(pv, runtime.GOOS, runtime.GOARCH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("plugin %q v%s has no binary for %s/%s", name, pv.Version, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
pluginDir := pluginHandler.EnsurePluginDir()
|
||||
|
||||
installer := plugin.NewInstaller()
|
||||
sp := newPluginSpinner(fmt.Sprintf("installing %s v%s", name, pv.Version))
|
||||
sp.Start()
|
||||
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||
sp.Stop()
|
||||
return err
|
||||
}
|
||||
sp.Stop()
|
||||
|
||||
logger.Successf("installed %s v%s", name, pv.Version)
|
||||
return nil
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||
)
|
||||
|
||||
var pluginListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List installed plugins",
|
||||
Long: `The plugin list command shows all installed plugins with their versions and paths.`,
|
||||
RunE: pluginListCmdRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
pluginCmd.AddCommand(pluginListCmd)
|
||||
}
|
||||
|
||||
func pluginListCmdRun(cmd *cobra.Command, args []string) error {
|
||||
pluginDir := pluginHandler.PluginDir()
|
||||
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||
if len(plugins) == 0 {
|
||||
cmd.Println("No plugins found")
|
||||
return nil
|
||||
}
|
||||
|
||||
header := []string{"NAME", "VERSION", "PATH"}
|
||||
var rows [][]string
|
||||
for _, p := range plugins {
|
||||
version := "manual"
|
||||
if receipt := plugin.ReadReceipt(pluginDir, p.Name); receipt != nil {
|
||||
version = receipt.Version
|
||||
}
|
||||
rows = append(rows, []string{p.Name, version, p.Path})
|
||||
}
|
||||
|
||||
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||
)
|
||||
|
||||
var pluginSearchCmd = &cobra.Command{
|
||||
Use: "search [query]",
|
||||
Short: "Search the plugin catalog",
|
||||
Long: `The plugin search command lists available plugins from the Flux plugin catalog.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: pluginSearchCmdRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
pluginCmd.AddCommand(pluginSearchCmd)
|
||||
}
|
||||
|
||||
func pluginSearchCmdRun(cmd *cobra.Command, args []string) error {
|
||||
catalogClient := newCatalogClient()
|
||||
catalog, err := catalogClient.FetchCatalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var query string
|
||||
if len(args) == 1 {
|
||||
query = strings.ToLower(args[0])
|
||||
}
|
||||
|
||||
pluginDir := pluginHandler.PluginDir()
|
||||
header := []string{"NAME", "DESCRIPTION", "INSTALLED"}
|
||||
var rows [][]string
|
||||
for _, entry := range catalog.Plugins {
|
||||
if query != "" {
|
||||
if !strings.Contains(strings.ToLower(entry.Name), query) &&
|
||||
!strings.Contains(strings.ToLower(entry.Description), query) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
installed := ""
|
||||
if receipt := plugin.ReadReceipt(pluginDir, entry.Name); receipt != nil {
|
||||
installed = receipt.Version
|
||||
}
|
||||
|
||||
rows = append(rows, []string{entry.Name, entry.Description, installed})
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
if query != "" {
|
||||
cmd.Printf("No plugins matching %q found in catalog\n", query)
|
||||
} else {
|
||||
cmd.Println("No plugins found in catalog")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user